Merge "[Satellite] Reading the datagram value from carrierConfig." into main
diff --git a/core/api/current.txt b/core/api/current.txt
index fd61800..b95295c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -10864,6 +10864,7 @@
     field public static final String IPSEC_SERVICE = "ipsec";
     field public static final String JOB_SCHEDULER_SERVICE = "jobscheduler";
     field public static final String KEYGUARD_SERVICE = "keyguard";
+    field @FlaggedApi("android.security.keystore_grant_api") public static final String KEYSTORE_SERVICE = "keystore";
     field public static final String LAUNCHER_APPS_SERVICE = "launcherapps";
     field @UiContext public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater";
     field public static final String LOCALE_SERVICE = "locale";
@@ -40166,6 +40167,14 @@
     method @NonNull public android.security.keystore.KeyProtection.Builder setUserPresenceRequired(boolean);
   }
 
+  @FlaggedApi("android.security.keystore_grant_api") public class KeyStoreManager {
+    method @NonNull public java.util.List<java.security.cert.X509Certificate> getGrantedCertificateChainFromId(long) throws android.security.keystore.KeyPermanentlyInvalidatedException, java.security.UnrecoverableKeyException;
+    method @NonNull public java.security.Key getGrantedKeyFromId(long) throws android.security.keystore.KeyPermanentlyInvalidatedException, java.security.UnrecoverableKeyException;
+    method @NonNull public java.security.KeyPair getGrantedKeyPairFromId(long) throws android.security.keystore.KeyPermanentlyInvalidatedException, java.security.UnrecoverableKeyException;
+    method public long grantKeyAccess(@NonNull String, int) throws android.security.KeyStoreException, java.security.UnrecoverableKeyException;
+    method public void revokeKeyAccess(@NonNull String, int) throws android.security.KeyStoreException, java.security.UnrecoverableKeyException;
+  }
+
   public class SecureKeyImportUnavailableException extends java.security.ProviderException {
     ctor public SecureKeyImportUnavailableException();
     ctor public SecureKeyImportUnavailableException(String);
@@ -52648,6 +52657,8 @@
     ctor public SurfaceView(android.content.Context, android.util.AttributeSet, int);
     ctor public SurfaceView(android.content.Context, android.util.AttributeSet, int, int);
     method public void applyTransactionToFrame(@NonNull android.view.SurfaceControl.Transaction);
+    method @FlaggedApi("android.view.flags.surface_view_get_surface_package") public void clearChildSurfacePackage();
+    method @FlaggedApi("android.view.flags.surface_view_get_surface_package") @Nullable public android.view.SurfaceControlViewHost.SurfacePackage getChildSurfacePackage();
     method @FlaggedApi("android.view.flags.surface_view_set_composition_order") public int getCompositionOrder();
     method public android.view.SurfaceHolder getHolder();
     method @Deprecated @Nullable public android.os.IBinder getHostToken();
diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java
index 4fc90ae..d84a4c1 100644
--- a/core/java/android/animation/AnimationHandler.java
+++ b/core/java/android/animation/AnimationHandler.java
@@ -81,6 +81,25 @@
      */
     private final ArrayList<WeakReference<Object>> mAnimatorRequestors = new ArrayList<>();
 
+    /**
+     * The callbacks which will invoke {@link Animator#notifyEndListeners(boolean)} on next frame.
+     * It is only used if {@link Animator#setPostNotifyEndListenerEnabled(boolean)} sets true.
+     */
+    private ArrayList<Runnable> mPendingEndAnimationListeners;
+
+    /**
+     * The value of {@link Choreographer#getVsyncId()} at the last animation frame.
+     * It is only used if {@link Animator#setPostNotifyEndListenerEnabled(boolean)} sets true.
+     */
+    private long mLastAnimationFrameVsyncId;
+
+    /**
+     * The value of {@link Choreographer#getVsyncId()} when calling
+     * {@link Animator#notifyEndListeners(boolean)}.
+     * It is only used if {@link Animator#setPostNotifyEndListenerEnabled(boolean)} sets true.
+     */
+    private long mEndAnimationFrameVsyncId;
+
     private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
         @Override
         public void doFrame(long frameTimeNanos) {
@@ -332,6 +351,39 @@
         }
     }
 
+    /**
+     * Returns the vsyncId of last animation frame if the given {@param currentVsyncId} matches
+     * the vsyncId from the end callback of animation. Otherwise it returns the given vsyncId.
+     * It only takes effect if {@link #postEndAnimationCallback(Runnable)} is called.
+     */
+    public long getLastAnimationFrameVsyncId(long currentVsyncId) {
+        return currentVsyncId == mEndAnimationFrameVsyncId && mLastAnimationFrameVsyncId != 0
+                ? mLastAnimationFrameVsyncId : currentVsyncId;
+    }
+
+    /** Runs the given callback on next frame to notify the end of the animation. */
+    public void postEndAnimationCallback(Runnable notifyEndAnimation) {
+        if (mPendingEndAnimationListeners == null) {
+            mPendingEndAnimationListeners = new ArrayList<>();
+        }
+        mPendingEndAnimationListeners.add(notifyEndAnimation);
+        if (mPendingEndAnimationListeners.size() > 1) {
+            return;
+        }
+        final Choreographer choreographer = Choreographer.getInstance();
+        mLastAnimationFrameVsyncId = choreographer.getVsyncId();
+        getProvider().postFrameCallback(frame -> {
+            mEndAnimationFrameVsyncId = choreographer.getVsyncId();
+            // The animation listeners can only get vsyncId of last animation frame in this frame
+            // by getLastAnimationFrameVsyncId(currentVsyncId).
+            while (mPendingEndAnimationListeners.size() > 0) {
+                mPendingEndAnimationListeners.remove(0).run();
+            }
+            mEndAnimationFrameVsyncId = 0;
+            mLastAnimationFrameVsyncId = 0;
+        });
+    }
+
     private void doAnimationFrame(long frameTime) {
         long currentTime = SystemClock.uptimeMillis();
         final int size = mAnimationCallbacks.size();
diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java
index c58624e..d1eb8e8 100644
--- a/core/java/android/animation/Animator.java
+++ b/core/java/android/animation/Animator.java
@@ -16,6 +16,7 @@
 
 package android.animation;
 
+import android.annotation.CallSuper;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.TestApi;
@@ -23,6 +24,7 @@
 import android.content.pm.ActivityInfo.Config;
 import android.content.res.ConstantState;
 import android.os.Build;
+import android.os.Trace;
 import android.util.LongArray;
 
 import java.util.ArrayList;
@@ -73,6 +75,13 @@
     private static long sBackgroundPauseDelay = 1000;
 
     /**
+     * If true, when the animation plays normally to the end, the callback
+     * {@link AnimatorListener#onAnimationEnd(Animator)} will be scheduled on the next frame.
+     * It is to avoid the last animation frame being delayed by the implementation of listeners.
+     */
+    static boolean sPostNotifyEndListenerEnabled;
+
+    /**
      * A cache of the values in a list. Used so that when calling the list, we have a copy
      * of it in case the list is modified while iterating. The array can be reused to avoid
      * allocation on every notification.
@@ -124,6 +133,14 @@
     }
 
     /**
+     * @see #sPostNotifyEndListenerEnabled
+     * @hide
+     */
+    public static void setPostNotifyEndListenerEnabled(boolean enable) {
+        sPostNotifyEndListenerEnabled = enable;
+    }
+
+    /**
      * Starts this animation. If the animation has a nonzero startDelay, the animation will start
      * running after that delay elapses. A non-delayed animation will have its initial
      * value(s) set immediately, followed by calls to
@@ -635,6 +652,28 @@
         }
     }
 
+    void notifyEndListenersFromEndAnimation(boolean isReversing, boolean postNotifyEndListener) {
+        if (postNotifyEndListener) {
+            AnimationHandler.getInstance().postEndAnimationCallback(
+                    () -> completeEndAnimation(isReversing, "postNotifyAnimEnd"));
+        } else {
+            completeEndAnimation(isReversing, "notifyAnimEnd");
+        }
+    }
+
+    @CallSuper
+    void completeEndAnimation(boolean isReversing, String notifyListenerTraceName) {
+        final boolean useTrace = mListeners != null && Trace.isTagEnabled(Trace.TRACE_TAG_VIEW);
+        if (useTrace) {
+            Trace.traceBegin(Trace.TRACE_TAG_VIEW, notifyListenerTraceName
+                    + "-" + getClass().getSimpleName());
+        }
+        notifyEndListeners(isReversing);
+        if (useTrace) {
+            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+        }
+    }
+
     /**
      * Calls <code>call</code> for every item in <code>list</code> with <code>animator</code> and
      * <code>isReverse</code> as parameters.
diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java
index ac37113..76098db 100644
--- a/core/java/android/animation/AnimatorSet.java
+++ b/core/java/android/animation/AnimatorSet.java
@@ -1442,6 +1442,8 @@
     }
 
     private void endAnimation() {
+        final boolean postNotifyEndListener = sPostNotifyEndListenerEnabled && mListeners != null
+                && mLastFrameTime > 0;
         mStarted = false;
         mLastFrameTime = -1;
         mFirstFrame = -1;
@@ -1453,7 +1455,12 @@
 
         // No longer receive callbacks
         removeAnimationCallback();
-        notifyEndListeners(mReversing);
+        notifyEndListenersFromEndAnimation(mReversing, postNotifyEndListener);
+    }
+
+    @Override
+    void completeEndAnimation(boolean isReversing, String notifyListenerTraceName) {
+        super.completeEndAnimation(isReversing, notifyListenerTraceName);
         removeAnimationEndListener();
         mSelfPulse = true;
         mReversing = false;
diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java
index 5de7f38..e849aba 100644
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -1289,6 +1289,8 @@
         if (mAnimationEndRequested) {
             return;
         }
+        final boolean postNotifyEndListener = sPostNotifyEndListenerEnabled && mListeners != null
+                && mLastFrameTime > 0;
         removeAnimationCallback();
 
         mAnimationEndRequested = true;
@@ -1303,15 +1305,20 @@
         mStartTime = -1;
         mRunning = false;
         mStarted = false;
-        notifyEndListeners(mReversing);
-        // mReversing needs to be reset *after* notifying the listeners for the end callbacks.
-        mReversing = false;
+        notifyEndListenersFromEndAnimation(mReversing, postNotifyEndListener);
         if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
             Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, getNameForTrace(),
                     System.identityHashCode(this));
         }
     }
 
+    @Override
+    void completeEndAnimation(boolean isReversing, String notifyListenerTraceName) {
+        super.completeEndAnimation(isReversing, notifyListenerTraceName);
+        // mReversing needs to be reset *after* notifying the listeners for the end callbacks.
+        mReversing = false;
+    }
+
     /**
      * Called internally to start an animation by adding it to the active animations list. Must be
      * called on the UI thread.
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index ba71afb..6e4c28f 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -3,50 +3,54 @@
 per-file ContextImpl.java = *
 
 # ActivityManager
-per-file ActivityManager* = file:/services/core/java/com/android/server/am/OWNERS
-per-file *ApplicationStartInfo* = file:/services/core/java/com/android/server/am/OWNERS
-per-file ApplicationErrorReport* = file:/services/core/java/com/android/server/am/OWNERS
-per-file ApplicationExitInfo* = file:/services/core/java/com/android/server/am/OWNERS
-per-file Application.java = file:/services/core/java/com/android/server/am/OWNERS
-per-file ApplicationLoaders.java = file:/services/core/java/com/android/server/am/OWNERS
-per-file ApplicationThreadConstants.java = file:/services/core/java/com/android/server/am/OWNERS
-per-file ContentProviderHolder* = file:/services/core/java/com/android/server/am/OWNERS
-per-file *ForegroundService* = file:/services/core/java/com/android/server/am/OWNERS
-per-file IActivityController.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IActivityManager.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IApplicationThread.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IAppTraceRetriever.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IForegroundServiceObserver.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IInstrumentationWatcher.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IntentService.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IServiceConnection.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IStopUserCallback.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IUidObserver.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file LoadedApk.java = file:/services/core/java/com/android/server/am/OWNERS
-per-file LocalActivityManager.java = file:/services/core/java/com/android/server/am/OWNERS
-per-file PendingIntent* = file:/services/core/java/com/android/server/am/OWNERS
-per-file *Process* = file:/services/core/java/com/android/server/am/OWNERS
-per-file ProfilerInfo* = file:/services/core/java/com/android/server/am/OWNERS
-per-file Service* = file:/services/core/java/com/android/server/am/OWNERS
-per-file SystemServiceRegistry.java = file:/services/core/java/com/android/server/am/OWNERS
-per-file *UserSwitchObserver* = file:/services/core/java/com/android/server/am/OWNERS
+per-file ActivityManager* = file:/ACTIVITY_MANAGER_OWNERS
+per-file Application.java = file:/ACTIVITY_MANAGER_OWNERS
+per-file ApplicationErrorReport* = file:/ACTIVITY_MANAGER_OWNERS
+per-file ApplicationLoaders.java = file:/ACTIVITY_MANAGER_OWNERS
+per-file ApplicationThreadConstants.java = file:/ACTIVITY_MANAGER_OWNERS
+per-file ContentProviderHolder* = file:/ACTIVITY_MANAGER_OWNERS
+per-file *ForegroundService* = file:/ACTIVITY_MANAGER_OWNERS
+per-file IActivityController.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IActivityManager.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IApplicationThread.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IAppTraceRetriever.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IForegroundServiceObserver.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IInstrumentationWatcher.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IntentService.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IServiceConnection.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IStopUserCallback.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IUidObserver.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file LoadedApk.java = file:/ACTIVITY_MANAGER_OWNERS
+per-file LocalActivityManager.java = file:/ACTIVITY_MANAGER_OWNERS
+per-file PendingIntent* = file:/ACTIVITY_MANAGER_OWNERS
+per-file *Process* = file:/ACTIVITY_MANAGER_OWNERS
+per-file ProfilerInfo* = file:/ACTIVITY_MANAGER_OWNERS
+per-file Service* = file:/ACTIVITY_MANAGER_OWNERS
+per-file SystemServiceRegistry.java = file:/ACTIVITY_MANAGER_OWNERS
+per-file *UserSwitchObserver* = file:/ACTIVITY_MANAGER_OWNERS
+
+# UI Automation
 per-file *UiAutomation* = file:/services/accessibility/OWNERS
 per-file *UiAutomation* = file:/core/java/android/permission/OWNERS
+
+# Game Manager
 per-file GameManager* = file:/GAME_MANAGER_OWNERS
 per-file GameMode* = file:/GAME_MANAGER_OWNERS
 per-file GameState* = file:/GAME_MANAGER_OWNERS
 per-file IGameManager* = file:/GAME_MANAGER_OWNERS
 per-file IGameMode* = file:/GAME_MANAGER_OWNERS
+
+# Background Starts
 per-file BackgroundStartPrivileges.java = file:/BAL_OWNERS
 per-file activity_manager.aconfig = file:/ACTIVITY_MANAGER_OWNERS
 
 # ActivityThread
-per-file ActivityThread.java = file:/services/core/java/com/android/server/am/OWNERS
+per-file ActivityThread.java = file:/ACTIVITY_MANAGER_OWNERS
 per-file ActivityThread.java = file:/services/core/java/com/android/server/wm/OWNERS
 per-file ActivityThread.java = file:RESOURCES_OWNERS
 
 # Alarm
-per-file *Alarm* = file:/apex/jobscheduler/OWNERS
+per-file *Alarm* = file:/apex/jobscheduler/ALARM_OWNERS
 
 # AppOps
 per-file *AppOp* = file:/core/java/android/permission/OWNERS
@@ -97,6 +101,8 @@
 
 # Performance
 per-file PropertyInvalidatedCache.java = file:/PERFORMANCE_OWNERS
+per-file *ApplicationStartInfo* = file:/PERFORMANCE_OWNERS
+per-file ApplicationExitInfo* = file:/PERFORMANCE_OWNERS
 per-file performance.aconfig = file:/PERFORMANCE_OWNERS
 
 # Pinner
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index c17da24..e4d3baa 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -16,6 +16,8 @@
 
 package android.app;
 
+import static android.text.TextUtils.formatSimple;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.TestApi;
@@ -30,11 +32,10 @@
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.FastPrintWriter;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 import java.io.ByteArrayOutputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -42,12 +43,14 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Random;
 import java.util.Set;
 import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicLong;
 
 /**
@@ -224,12 +227,24 @@
     }
 
     /**
-     * Reserved nonce values.  Use isReservedNonce() to test for a reserved value.  Note
-     * that all values cause the cache to be skipped.
+     * Reserved nonce values.  Use isReservedNonce() to test for a reserved value.  Note that all
+     * reserved values cause the cache to be skipped.
      */
+    // This is the initial value of all cache keys.  It is changed when a cache is invalidated.
     private static final int NONCE_UNSET = 0;
+    // This value is used in two ways.  First, it is used internally to indicate that the cache is
+    // disabled for the current query.  Secondly, it is used to globally disable the cache across
+    // the entire system.  Once a cache is disabled, there is no way to enable it again.  The
+    // global behavior is unused and will likely be removed in the future.
     private static final int NONCE_DISABLED = 1;
+    // The cache is corked, which means that clients must act as though the cache is always
+    // invalid.  This is used when the server is processing updates that continuously invalidate
+    // caches.  Rather than issuing individual invalidations (which has a performance penalty),
+    // the server corks the caches at the start of the process and uncorks at the end of the
+    // process.
     private static final int NONCE_CORKED = 2;
+    // The cache is bypassed for the current query.  Unlike UNSET and CORKED, this value is never
+    // written to global store.
     private static final int NONCE_BYPASS = 3;
 
     private static boolean isReservedNonce(long n) {
@@ -237,15 +252,27 @@
     }
 
     /**
-     * The names of the nonces
+     * The names of the reserved nonces.
      */
     private static final String[] sNonceName =
             new String[]{ "unset", "disabled", "corked", "bypass" };
 
+    // The standard tag for logging.
     private static final String TAG = "PropertyInvalidatedCache";
+
+    // Set this true to enable very chatty logging.  Never commit this true.
     private static final boolean DEBUG = false;
+
+    // Set this true to enable cache verification.  On every cache hit, the cache will compare the
+    // cached value to a value pulled directly from the source.  This completely negates any
+    // performance advantage of the cache.  Enable it only to test if a particular cache is not
+    // being properly invalidated.
     private static final boolean VERIFY = false;
 
+    // The test mode. This is only used to ensure that the test functions setTestMode() and
+    // testPropertyName() are used correctly.
+    private static boolean sTestMode = false;
+
     /**
      * The object-private lock.
      */
@@ -277,32 +304,17 @@
     private static final Object sCorkLock = new Object();
 
     /**
-     * Record the number of invalidate or cork calls that were nops because the cache was already
-     * corked.  This is static because invalidation is done in a static context.  Entries are
-     * indexed by the cache property.
-     */
-    @GuardedBy("sCorkLock")
-    private static final HashMap<String, Long> sCorkedInvalidates = new HashMap<>();
-
-    /**
-     * A map of cache keys that we've "corked". (The values are counts.)  When a cache key is
-     * corked, we skip the cache invalidate when the cache key is in the unset state --- that
-     * is, when a cache key is corked, an invalidation does not enable the cache if somebody
-     * else hasn't disabled it.
-     */
-    @GuardedBy("sCorkLock")
-    private static final HashMap<String, Integer> sCorks = new HashMap<>();
-
-    /**
      * A lock for the global list of caches and cache keys.  This must never be taken inside mLock
      * or sCorkLock.
      */
     private static final Object sGlobalLock = new Object();
 
     /**
-     * A map of cache keys that have been disabled in the local process.  When a key is
-     * disabled locally, existing caches are disabled and the key is saved in this map.
-     * Future cache instances that use the same key will be disabled in their constructor.
+     * A map of cache keys that have been disabled in the local process.  When a key is disabled
+     * locally, existing caches are disabled and the key is saved in this map.  Future cache
+     * instances that use the same key will be disabled in their constructor.  Note that "disabled"
+     * means the cache is not used in this process.  Invalidation still proceeds normally, because
+     * the cache may be used in other processes.
      */
     @GuardedBy("sGlobalLock")
     private static final HashSet<String> sDisabledKeys = new HashSet<>();
@@ -315,14 +327,6 @@
     private static final WeakHashMap<PropertyInvalidatedCache, Void> sCaches = new WeakHashMap<>();
 
     /**
-     * Counts of the number of times a cache key was invalidated.  Invalidation occurs in a static
-     * context with no cache object available, so this is a static map.  Entries are indexed by
-     * the cache property.
-     */
-    @GuardedBy("sGlobalLock")
-    private static final HashMap<String, Long> sInvalidates = new HashMap<>();
-
-    /**
      * If sEnabled is false then all cache operations are stubbed out.  Set
      * it to false inside test processes.
      */
@@ -334,12 +338,6 @@
     private final String mPropertyName;
 
     /**
-     * Handle to the {@code mPropertyName} property, transitioning to non-{@code null} once the
-     * property exists on the system.
-     */
-    private volatile SystemProperties.Handle mPropertyHandle;
-
-    /**
      * The name by which this cache is known.  This should normally be the
      * binder call that is being cached, but the constructors default it to
      * the property name.
@@ -369,7 +367,13 @@
     private final LinkedHashMap<Query, Result> mCache;
 
     /**
-     * The last value of the {@code mPropertyHandle} that we observed.
+     * The nonce handler for this cache.
+     */
+    @GuardedBy("mLock")
+    private final NonceHandler mNonce;
+
+    /**
+     * The last nonce value that was observed.
      */
     @GuardedBy("mLock")
     private long mLastSeenNonce = NONCE_UNSET;
@@ -385,6 +389,358 @@
     private final int mMaxEntries;
 
     /**
+     * A class to manage cache keys.  There is a single instance of this class for each unique key
+     * that is shared by all cache instances that use that key.  This class is abstract; subclasses
+     * use different storage mechanisms for the nonces.
+     */
+    private static abstract class NonceHandler {
+        // The name of the nonce.
+        final String mName;
+
+        // A lock to synchronize corking and invalidation.
+        protected final Object mLock = new Object();
+
+        // Count the number of times the property name was invalidated.
+        @GuardedBy("mLock")
+        private int mInvalidated = 0;
+
+        // Count the number of times invalidate or cork calls were nops because the cache was
+        // already corked.
+        @GuardedBy("mLock")
+        private int mCorkedInvalidates = 0;
+
+        // Count the number of corks against this property name.  This is not a statistic.  It
+        // increases when the property is corked and decreases when the property is uncorked.
+        // Invalidation requests are ignored when the cork count is greater than zero.
+        @GuardedBy("mLock")
+        private int mCorks = 0;
+
+        // True if this handler is in test mode.  If it is in test mode, then nonces are stored
+        // and retrieved from mTestNonce.
+        @GuardedBy("mLock")
+        private boolean mTestMode = false;
+
+        /**
+         * The local value of the handler, used during testing but also used directly by the
+         * NonceLocal handler.
+         */
+        @GuardedBy("mLock")
+        protected long mTestNonce = NONCE_UNSET;
+
+        /**
+         * The methods to get and set a nonce from whatever storage is being used.  mLock may be
+         * held when these methods are called.  Implementations that take locks must behave as
+         * though mLock could be held.
+         */
+        abstract long getNonceInternal();
+        abstract void setNonceInternal(long value);
+
+        NonceHandler(@NonNull String name) {
+            mName = name;
+        }
+
+        /**
+         * Get a nonce from storage.  If the handler is in test mode, the nonce is returned from
+         * the local mTestNonce.
+         */
+        long getNonce() {
+            synchronized (mLock) {
+                if (mTestMode) return mTestNonce;
+            }
+            return getNonceInternal();
+        }
+
+        /**
+         * Write a nonce to storage.  If the handler is in test mode, the nonce is written to the
+         * local mTestNonce and storage is not affected.
+         */
+        void setNonce(long val) {
+            synchronized (mLock) {
+                if (mTestMode) {
+                    mTestNonce = val;
+                    return;
+                }
+            }
+            setNonceInternal(val);
+        }
+
+        /**
+         * Write the invalidation nonce for the property.
+         */
+        void invalidate() {
+            if (!sEnabled) {
+                if (DEBUG) {
+                    Log.d(TAG, formatSimple("cache invalidate %s suppressed", mName));
+                }
+                return;
+            }
+
+            synchronized (mLock) {
+                if (mCorks > 0) {
+                    if (DEBUG) {
+                        Log.d(TAG, "ignoring invalidation due to cork: " + mName);
+                    }
+                    mCorkedInvalidates++;
+                    return;
+                }
+
+                final long nonce = getNonce();
+                if (nonce == NONCE_DISABLED) {
+                    if (DEBUG) {
+                        Log.d(TAG, "refusing to invalidate disabled cache: " + mName);
+                    }
+                    return;
+                }
+
+                long newValue;
+                do {
+                    newValue = NoPreloadHolder.next();
+                } while (isReservedNonce(newValue));
+                if (DEBUG) {
+                    Log.d(TAG, formatSimple(
+                        "invalidating cache [%s]: [%s] -> [%s]",
+                        mName, nonce, Long.toString(newValue)));
+                }
+                // There is a small race with concurrent disables here.  A compare-and-exchange
+                // property operation would be required to eliminate the race condition.
+                setNonce(newValue);
+                mInvalidated++;
+            }
+        }
+
+        void cork() {
+            if (!sEnabled) {
+                if (DEBUG) {
+                    Log.d(TAG, formatSimple("cache corking %s suppressed", mName));
+                }
+                return;
+            }
+
+            synchronized (mLock) {
+                int numberCorks = mCorks;
+                if (DEBUG) {
+                    Log.d(TAG, formatSimple(
+                        "corking %s: numberCorks=%s", mName, numberCorks));
+                }
+
+                // If we're the first ones to cork this cache, set the cache to the corked state so
+                // existing caches talk directly to their services while we've corked updates.
+                // Make sure we don't clobber a disabled cache value.
+
+                // TODO: we can skip this property write and leave the cache enabled if the
+                // caller promises not to make observable changes to the cache backing state before
+                // uncorking the cache, e.g., by holding a read lock across the cork-uncork pair.
+                // Implement this more dangerous mode of operation if necessary.
+                if (numberCorks == 0) {
+                    final long nonce = getNonce();
+                    if (nonce != NONCE_UNSET && nonce != NONCE_DISABLED) {
+                        setNonce(NONCE_CORKED);
+                    }
+                } else {
+                    mCorkedInvalidates++;
+                }
+                mCorks++;
+                if (DEBUG) {
+                    Log.d(TAG, "corked: " + mName);
+                }
+            }
+        }
+
+        void uncork() {
+            if (!sEnabled) {
+                if (DEBUG) {
+                    Log.d(TAG, formatSimple("cache uncorking %s suppressed", mName));
+                }
+                return;
+            }
+
+            synchronized (mLock) {
+                int numberCorks = --mCorks;
+                if (DEBUG) {
+                    Log.d(TAG, formatSimple(
+                        "uncorking %s: numberCorks=%s", mName, numberCorks));
+                }
+
+                if (numberCorks < 0) {
+                    throw new AssertionError("cork underflow: " + mName);
+                }
+                if (numberCorks == 0) {
+                    // The property is fully uncorked and can be invalidated normally.
+                    invalidate();
+                    if (DEBUG) {
+                        Log.d(TAG, "uncorked: " + mName);
+                    }
+                }
+            }
+        }
+
+        /**
+         * Globally (that is, system-wide) disable all caches that use this key.  There is no way
+         * to re-enable these caches.
+         */
+        void disable() {
+            if (!sEnabled) {
+                return;
+            }
+            synchronized (mLock) {
+                setNonce(NONCE_DISABLED);
+            }
+        }
+
+        /**
+         * Put this handler in or out of test mode.  Regardless of the current and next mode, the
+         * test nonce variable is reset to UNSET.
+         */
+        void setTestMode(boolean mode) {
+            synchronized (mLock) {
+                mTestMode = mode;
+                mTestNonce = NONCE_UNSET;
+            }
+        }
+
+        /**
+         * Return the statistics associated with the key.  These statistics are not associated
+         * with any individual cache.
+         */
+        record Stats(int invalidated, int corkedInvalidates) {}
+        Stats getStats() {
+            synchronized (mLock) {
+                return new Stats(mInvalidated, mCorkedInvalidates);
+            }
+        }
+    }
+
+    /**
+     * Manage nonces that are stored in a system property.
+     */
+    private static final class NonceSysprop extends NonceHandler {
+        // A handle to the property, for fast lookups.
+        private volatile SystemProperties.Handle mHandle;
+
+        NonceSysprop(@NonNull String name) {
+            super(name);
+        }
+
+        /**
+         * Retrieve the nonce from the system property.  If the handle is null, this method
+         * attempts to create a handle.  If handle creation fails, the method returns UNSET.  If
+         * the handle is not null, the method returns a value read via the handle.  This read
+         * occurs outside any lock.
+         */
+        @Override
+        long getNonceInternal() {
+            if (mHandle == null) {
+                synchronized (mLock) {
+                    if (mHandle == null) {
+                        mHandle = SystemProperties.find(mName);
+                        if (mHandle == null) {
+                            return NONCE_UNSET;
+                        }
+                    }
+                }
+            }
+            return mHandle.getLong(NONCE_UNSET);
+        }
+
+        /**
+         * Write a nonce to a system property.
+         */
+        @Override
+        void setNonceInternal(long value) {
+            // Failing to set the nonce is a fatal error.  Failures setting a system property have
+            // been reported; given that the failure is probably transient, this function includes
+            // a retry.
+            final String str = Long.toString(value);
+            RuntimeException failure = null;
+            for (int attempt = 0; attempt < PROPERTY_FAILURE_RETRY_LIMIT; attempt++) {
+                try {
+                    SystemProperties.set(mName, str);
+                    if (attempt > 0) {
+                        // This log is not guarded.  Based on known bug reports, it should
+                        // occur once a week or less.  The purpose of the log message is to
+                        // identify the retries as a source of delay that might be otherwise
+                        // be attributed to the cache itself.
+                        Log.w(TAG, "Nonce set after " + attempt + " tries");
+                    }
+                    return;
+                } catch (RuntimeException e) {
+                    if (failure == null) {
+                        failure = e;
+                    }
+                    try {
+                        Thread.sleep(PROPERTY_FAILURE_RETRY_DELAY_MILLIS);
+                    } catch (InterruptedException x) {
+                        // Ignore this exception.  The desired delay is only approximate and
+                        // there is no issue if the sleep sometimes terminates early.
+                    }
+                }
+            }
+            // This point is reached only if SystemProperties.set() fails at least once.
+            // Rethrow the first exception that was received.
+            throw failure;
+        }
+    }
+
+    /**
+     * SystemProperties and shared storage are protected and cannot be written by random
+     * processes.  So, for testing purposes, the NonceLocal handler stores the nonce locally.  The
+     * NonceLocal uses the mTestNonce in the superclass, regardless of test mode.
+     */
+    private static class NonceLocal extends NonceHandler {
+        // The saved nonce.
+        private long mValue;
+
+        NonceLocal(@NonNull String name) {
+            super(name);
+        }
+
+        @Override
+        long getNonceInternal() {
+            return mTestNonce;
+        }
+
+        @Override
+        void setNonceInternal(long value) {
+            mTestNonce = value;
+        }
+    }
+
+    /**
+     * Complete key prefixes.
+     */
+    private static final String PREFIX_TEST = CACHE_KEY_PREFIX + "." + MODULE_TEST + ".";
+
+    /**
+     * A static list of nonce handlers, indexed by name.  NonceHandlers can be safely shared by
+     * multiple threads, and can therefore be shared by multiple instances of the same cache, and
+     * with static calls (see {@link #invalidateCache}.  Addition and removal are guarded by the
+     * global lock, to ensure that duplicates are not created.
+     */
+    private static final ConcurrentHashMap<String, NonceHandler> sHandlers
+            = new ConcurrentHashMap<>();
+
+    /**
+     * Return the proper nonce handler, based on the property name.
+     */
+    private static NonceHandler getNonceHandler(@NonNull String name) {
+        NonceHandler h = sHandlers.get(name);
+        if (h == null) {
+            synchronized (sGlobalLock) {
+                h = sHandlers.get(name);
+                if (h == null) {
+                    if (name.startsWith(PREFIX_TEST)) {
+                        h = new NonceLocal(name);
+                    } else {
+                        h = new NonceSysprop(name);
+                    }
+                    sHandlers.put(name, h);
+                }
+            }
+        }
+        return h;
+    }
+
+    /**
      * Make a new property invalidated cache.  This constructor names the cache after the
      * property name.  New clients should prefer the constructor that takes an explicit
      * cache name.
@@ -417,6 +773,7 @@
         mPropertyName = propertyName;
         validateCacheKey(mPropertyName);
         mCacheName = cacheName;
+        mNonce = getNonceHandler(mPropertyName);
         mMaxEntries = maxEntries;
         mComputer = new DefaultComputer<>(this);
         mCache = createMap();
@@ -441,6 +798,7 @@
         mPropertyName = createPropertyName(module, api);
         validateCacheKey(mPropertyName);
         mCacheName = cacheName;
+        mNonce = getNonceHandler(mPropertyName);
         mMaxEntries = maxEntries;
         mComputer = computer;
         mCache = createMap();
@@ -484,130 +842,69 @@
     }
 
     /**
-     * SystemProperties are protected and cannot be written (or read, usually) by random
-     * processes.  So, for testing purposes, the methods have a bypass mode that reads and
-     * writes to a HashMap and does not go out to the SystemProperties at all.
-     */
-
-    // If true, the cache might be under test.  If false, there is no testing in progress.
-    private static volatile boolean sTesting = false;
-
-    // If sTesting is true then keys that are under test are in this map.
-    private static final HashMap<String, Long> sTestingPropertyMap = new HashMap<>();
-
-    /**
-     * Enable or disable testing.  The testing property map is cleared every time this
-     * method is called.
+     * Enable or disable testing.  The protocol requires that the mode toggle: for instance, it is
+     * illegal to clear the test mode if the test mode is already off.  The purpose is solely to
+     * ensure that test clients do not forget to use the test mode properly, even though the
+     * current logic does not care.
      * @hide
      */
     @TestApi
     public static void setTestMode(boolean mode) {
-        sTesting = mode;
-        synchronized (sTestingPropertyMap) {
-            sTestingPropertyMap.clear();
+        synchronized (sGlobalLock) {
+            if (sTestMode == mode) {
+                throw new IllegalStateException("cannot set test mode redundantly: mode=" + mode);
+            }
+            sTestMode = mode;
+            if (mode) {
+                // No action when testing begins.
+            } else {
+                resetAfterTestLocked();
+            }
         }
     }
 
     /**
-     * Enable testing the specific cache key.  Only keys in the map are subject to testing.
-     * There is no method to stop testing a property name.  Just disable the test mode.
+     * Clean up when testing ends. All handlers are reset out of test mode.  NonceLocal handlers
+     * (MODULE_TEST) are reset to the NONCE_UNSET state.  This has no effect on any other handlers
+     * that were not originally in test mode.
      */
-    private static void testPropertyName(@NonNull String name) {
-        synchronized (sTestingPropertyMap) {
-            sTestingPropertyMap.put(name, (long) NONCE_UNSET);
+    @GuardedBy("sGlobalLock")
+    private static void resetAfterTestLocked() {
+        for (Iterator<String> e = sHandlers.keys().asIterator(); e.hasNext(); ) {
+            String s = e.next();
+            final NonceHandler h = sHandlers.get(s);
+            h.setTestMode(false);
         }
     }
 
     /**
-     * Enable testing the specific cache key.  Only keys in the map are subject to testing.
-     * There is no method to stop testing a property name.  Just disable the test mode.
+     * Enable testing the specific cache key.  This API allows a test process to invalidate caches
+     * for which it would not otherwise have permission.  Caches in test mode do NOT write their
+     * values to the system properties.  The effect is local to the current process.  Test mode
+     * must be true when this method is called.
      * @hide
      */
     @TestApi
     public void testPropertyName() {
-        testPropertyName(mPropertyName);
+        synchronized (sGlobalLock) {
+            if (sTestMode == false) {
+                throw new IllegalStateException("cannot test property name with test mode off");
+            }
+            mNonce.setTestMode(true);
+        }
     }
 
-    // Read the system property associated with the current cache.  This method uses the
-    // handle for faster reading.
+    // Read the nonce associated with the current cache.
+    @GuardedBy("mLock")
     private long getCurrentNonce() {
-        if (sTesting) {
-            synchronized (sTestingPropertyMap) {
-                Long n = sTestingPropertyMap.get(mPropertyName);
-                if (n != null) {
-                    return n;
-                }
-            }
-        }
-
-        SystemProperties.Handle handle = mPropertyHandle;
-        if (handle == null) {
-            handle = SystemProperties.find(mPropertyName);
-            if (handle == null) {
-                return NONCE_UNSET;
-            }
-            mPropertyHandle = handle;
-        }
-        return handle.getLong(NONCE_UNSET);
-    }
-
-    // Write the nonce in a static context.  No handle is available.
-    private static void setNonce(String name, long val) {
-        if (sTesting) {
-            synchronized (sTestingPropertyMap) {
-                Long n = sTestingPropertyMap.get(name);
-                if (n != null) {
-                    sTestingPropertyMap.put(name, val);
-                    return;
-                }
-            }
-        }
-        RuntimeException failure = null;
-        for (int attempt = 0; attempt < PROPERTY_FAILURE_RETRY_LIMIT; attempt++) {
-            try {
-                SystemProperties.set(name, Long.toString(val));
-                if (attempt > 0) {
-                    // This log is not guarded.  Based on known bug reports, it should
-                    // occur once a week or less.  The purpose of the log message is to
-                    // identify the retries as a source of delay that might be otherwise
-                    // be attributed to the cache itself.
-                    Log.w(TAG, "Nonce set after " + attempt + " tries");
-                }
-                return;
-            } catch (RuntimeException e) {
-                if (failure == null) {
-                    failure = e;
-                }
-                try {
-                    Thread.sleep(PROPERTY_FAILURE_RETRY_DELAY_MILLIS);
-                } catch (InterruptedException x) {
-                    // Ignore this exception.  The desired delay is only approximate and
-                    // there is no issue if the sleep sometimes terminates early.
-                }
-            }
-        }
-        // This point is reached only if SystemProperties.set() fails at least once.
-        // Rethrow the first exception that was received.
-        throw failure;
-    }
-
-    // Set the nonce in a static context.  No handle is available.
-    private static long getNonce(String name) {
-        if (sTesting) {
-            synchronized (sTestingPropertyMap) {
-                Long n = sTestingPropertyMap.get(name);
-                if (n != null) {
-                    return n;
-                }
-            }
-        }
-        return SystemProperties.getLong(name, NONCE_UNSET);
+        return mNonce.getNonce();
     }
 
     /**
-     * Forget all cached values.
-     * TODO(216112648) remove this as a public API.  Clients should invalidate caches, not clear
-     * them.
+     * Forget all cached values.  This is used by a client when the server exits.  Since the
+     * server has exited, the cache values are no longer valid, but the server is no longer
+     * present to invalidate the cache.  Note that this is not necessary if the server is
+     * system_server, because the entire operating system reboots if that process exits.
      * @hide
      */
     public final void clear() {
@@ -674,7 +971,7 @@
     }
 
     /**
-     * Disable the use of this cache in this process.  This method is using internally and during
+     * Disable the use of this cache in this process.  This method is used internally and during
      * testing.  To disable a cache in normal code, use disableLocal().  A disabled cache cannot
      * be re-enabled.
      * @hide
@@ -783,7 +1080,7 @@
 
                 if (DEBUG) {
                     if (!mDisabled) {
-                        Log.d(TAG, TextUtils.formatSimple(
+                        Log.d(TAG, formatSimple(
                             "cache %s %s for %s",
                             cacheName(), sNonceName[(int) currentNonce], queryToString(query)));
                     }
@@ -798,7 +1095,7 @@
                     if (cachedResult != null) mHits++;
                 } else {
                     if (DEBUG) {
-                        Log.d(TAG, TextUtils.formatSimple(
+                        Log.d(TAG, formatSimple(
                             "clearing cache %s of %d entries because nonce changed [%s] -> [%s]",
                             cacheName(), mCache.size(),
                             mLastSeenNonce, currentNonce));
@@ -824,7 +1121,7 @@
                     if (currentNonce != afterRefreshNonce) {
                         currentNonce = afterRefreshNonce;
                         if (DEBUG) {
-                            Log.d(TAG, TextUtils.formatSimple(
+                            Log.d(TAG, formatSimple(
                                     "restarting %s %s because nonce changed in refresh",
                                     cacheName(),
                                     queryToString(query)));
@@ -895,20 +1192,18 @@
      * @param name Name of the cache-key property to invalidate
      */
     private static void disableSystemWide(@NonNull String name) {
-        if (!sEnabled) {
-            return;
-        }
-        setNonce(name, NONCE_DISABLED);
+        getNonceHandler(name).disable();
     }
 
     /**
-     * Non-static convenience version of invalidateCache() for situations in which only a single
-     * PropertyInvalidatedCache is keyed on a particular property value.
+     * Non-static version of invalidateCache() for situations in which a cache instance is
+     * available.  This is slightly faster than than the static versions because it does not have
+     * to look up the NonceHandler for a given property name.
      * @hide
      */
     @TestApi
     public void invalidateCache() {
-        invalidateCache(mPropertyName);
+        mNonce.invalidate();
     }
 
     /**
@@ -931,59 +1226,7 @@
      * @hide
      */
     public static void invalidateCache(@NonNull String name) {
-        if (!sEnabled) {
-            if (DEBUG) {
-                Log.w(TAG, TextUtils.formatSimple(
-                    "cache invalidate %s suppressed", name));
-            }
-            return;
-        }
-
-        // Take the cork lock so invalidateCache() racing against corkInvalidations() doesn't
-        // clobber a cork-written NONCE_UNSET with a cache key we compute before the cork.
-        // The property service is single-threaded anyway, so we don't lose any concurrency by
-        // taking the cork lock around cache invalidations.  If we see contention on this lock,
-        // we're invalidating too often.
-        synchronized (sCorkLock) {
-            Integer numberCorks = sCorks.get(name);
-            if (numberCorks != null && numberCorks > 0) {
-                if (DEBUG) {
-                    Log.d(TAG, "ignoring invalidation due to cork: " + name);
-                }
-                final long count = sCorkedInvalidates.getOrDefault(name, (long) 0);
-                sCorkedInvalidates.put(name, count + 1);
-                return;
-            }
-            invalidateCacheLocked(name);
-        }
-    }
-
-    @GuardedBy("sCorkLock")
-    private static void invalidateCacheLocked(@NonNull String name) {
-        // There's no race here: we don't require that values strictly increase, but instead
-        // only that each is unique in a single runtime-restart session.
-        final long nonce = getNonce(name);
-        if (nonce == NONCE_DISABLED) {
-            if (DEBUG) {
-                Log.d(TAG, "refusing to invalidate disabled cache: " + name);
-            }
-            return;
-        }
-
-        long newValue;
-        do {
-            newValue = NoPreloadHolder.next();
-        } while (isReservedNonce(newValue));
-        if (DEBUG) {
-            Log.d(TAG, TextUtils.formatSimple(
-                    "invalidating cache [%s]: [%s] -> [%s]",
-                    name, nonce, Long.toString(newValue)));
-        }
-        // There is a small race with concurrent disables here.  A compare-and-exchange
-        // property operation would be required to eliminate the race condition.
-        setNonce(name, newValue);
-        long invalidateCount = sInvalidates.getOrDefault(name, (long) 0);
-        sInvalidates.put(name, ++invalidateCount);
+        getNonceHandler(name).invalidate();
     }
 
     /**
@@ -1000,43 +1243,7 @@
      * @hide
      */
     public static void corkInvalidations(@NonNull String name) {
-        if (!sEnabled) {
-            if (DEBUG) {
-                Log.w(TAG, TextUtils.formatSimple(
-                    "cache cork %s suppressed", name));
-            }
-            return;
-        }
-
-        synchronized (sCorkLock) {
-            int numberCorks = sCorks.getOrDefault(name, 0);
-            if (DEBUG) {
-                Log.d(TAG, TextUtils.formatSimple(
-                        "corking %s: numberCorks=%s", name, numberCorks));
-            }
-
-            // If we're the first ones to cork this cache, set the cache to the corked state so
-            // existing caches talk directly to their services while we've corked updates.
-            // Make sure we don't clobber a disabled cache value.
-
-            // TODO(dancol): we can skip this property write and leave the cache enabled if the
-            // caller promises not to make observable changes to the cache backing state before
-            // uncorking the cache, e.g., by holding a read lock across the cork-uncork pair.
-            // Implement this more dangerous mode of operation if necessary.
-            if (numberCorks == 0) {
-                final long nonce = getNonce(name);
-                if (nonce != NONCE_UNSET && nonce != NONCE_DISABLED) {
-                    setNonce(name, NONCE_CORKED);
-                }
-            } else {
-                final long count = sCorkedInvalidates.getOrDefault(name, (long) 0);
-                sCorkedInvalidates.put(name, count + 1);
-            }
-            sCorks.put(name, numberCorks + 1);
-            if (DEBUG) {
-                Log.d(TAG, "corked: " + name);
-            }
-        }
+        getNonceHandler(name).cork();
     }
 
     /**
@@ -1048,34 +1255,7 @@
      * @hide
      */
     public static void uncorkInvalidations(@NonNull String name) {
-        if (!sEnabled) {
-            if (DEBUG) {
-                Log.w(TAG, TextUtils.formatSimple(
-                        "cache uncork %s suppressed", name));
-            }
-            return;
-        }
-
-        synchronized (sCorkLock) {
-            int numberCorks = sCorks.getOrDefault(name, 0);
-            if (DEBUG) {
-                Log.d(TAG, TextUtils.formatSimple(
-                        "uncorking %s: numberCorks=%s", name, numberCorks));
-            }
-
-            if (numberCorks < 1) {
-                throw new AssertionError("cork underflow: " + name);
-            }
-            if (numberCorks == 1) {
-                sCorks.remove(name);
-                invalidateCacheLocked(name);
-                if (DEBUG) {
-                    Log.d(TAG, "uncorked: " + name);
-                }
-            } else {
-                sCorks.put(name, numberCorks - 1);
-            }
-        }
+        getNonceHandler(name).uncork();
     }
 
     /**
@@ -1104,6 +1284,8 @@
         @GuardedBy("mLock")
         private Handler mHandler;
 
+        private NonceHandler mNonce;
+
         public AutoCorker(@NonNull String propertyName) {
             this(propertyName, DEFAULT_AUTO_CORK_DELAY_MS);
         }
@@ -1117,31 +1299,35 @@
         }
 
         public void autoCork() {
+            synchronized (mLock) {
+                if (mNonce == null) {
+                    mNonce = getNonceHandler(mPropertyName);
+                }
+            }
+
             if (getLooper() == null) {
                 // We're not ready to auto-cork yet, so just invalidate the cache immediately.
                 if (DEBUG) {
                     Log.w(TAG, "invalidating instead of autocorking early in init: "
                             + mPropertyName);
                 }
-                PropertyInvalidatedCache.invalidateCache(mPropertyName);
+                mNonce.invalidate();
                 return;
             }
             synchronized (mLock) {
                 boolean alreadyQueued = mUncorkDeadlineMs >= 0;
                 if (DEBUG) {
-                    Log.w(TAG, TextUtils.formatSimple(
+                    Log.d(TAG, formatSimple(
                             "autoCork %s mUncorkDeadlineMs=%s", mPropertyName,
                             mUncorkDeadlineMs));
                 }
                 mUncorkDeadlineMs = SystemClock.uptimeMillis() + mAutoCorkDelayMs;
                 if (!alreadyQueued) {
                     getHandlerLocked().sendEmptyMessageAtTime(0, mUncorkDeadlineMs);
-                    PropertyInvalidatedCache.corkInvalidations(mPropertyName);
+                    mNonce.cork();
                 } else {
-                    synchronized (sCorkLock) {
-                        final long count = sCorkedInvalidates.getOrDefault(mPropertyName, (long) 0);
-                        sCorkedInvalidates.put(mPropertyName, count + 1);
-                    }
+                    // Count this as a corked invalidation.
+                    mNonce.invalidate();
                 }
             }
         }
@@ -1149,7 +1335,7 @@
         private void handleMessage(Message msg) {
             synchronized (mLock) {
                 if (DEBUG) {
-                    Log.w(TAG, TextUtils.formatSimple(
+                    Log.d(TAG, formatSimple(
                             "handleMsesage %s mUncorkDeadlineMs=%s",
                             mPropertyName, mUncorkDeadlineMs));
                 }
@@ -1161,7 +1347,7 @@
                 if (mUncorkDeadlineMs > nowMs) {
                     mUncorkDeadlineMs = nowMs + mAutoCorkDelayMs;
                     if (DEBUG) {
-                        Log.w(TAG, TextUtils.formatSimple(
+                        Log.d(TAG, formatSimple(
                                         "scheduling uncork at %s",
                                         mUncorkDeadlineMs));
                     }
@@ -1169,10 +1355,10 @@
                     return;
                 }
                 if (DEBUG) {
-                    Log.w(TAG, "automatic uncorking " + mPropertyName);
+                    Log.d(TAG, "automatic uncorking " + mPropertyName);
                 }
                 mUncorkDeadlineMs = -1;
-                PropertyInvalidatedCache.uncorkInvalidations(mPropertyName);
+                mNonce.uncork();
             }
         }
 
@@ -1207,7 +1393,7 @@
             Result resultToCompare = recompute(query);
             boolean nonceChanged = (getCurrentNonce() != mLastSeenNonce);
             if (!nonceChanged && !resultEquals(proposedResult, resultToCompare)) {
-                Log.e(TAG, TextUtils.formatSimple(
+                Log.e(TAG, formatSimple(
                         "cache %s inconsistent for %s is %s should be %s",
                         cacheName(), queryToString(query),
                         proposedResult, resultToCompare));
@@ -1284,17 +1470,9 @@
     /**
      * Returns a list of caches alive at the current time.
      */
-    @GuardedBy("sGlobalLock")
     private static @NonNull ArrayList<PropertyInvalidatedCache> getActiveCaches() {
-        return new ArrayList<PropertyInvalidatedCache>(sCaches.keySet());
-    }
-
-    /**
-     * Returns a list of the active corks in a process.
-     */
-    private static @NonNull ArrayList<Map.Entry<String, Integer>> getActiveCorks() {
-        synchronized (sCorkLock) {
-            return new ArrayList<Map.Entry<String, Integer>>(sCorks.entrySet());
+        synchronized (sGlobalLock) {
+            return new ArrayList<PropertyInvalidatedCache>(sCaches.keySet());
         }
     }
 
@@ -1361,32 +1539,27 @@
             return;
         }
 
-        long invalidateCount;
-        long corkedInvalidates;
-        synchronized (sCorkLock) {
-            invalidateCount = sInvalidates.getOrDefault(mPropertyName, (long) 0);
-            corkedInvalidates = sCorkedInvalidates.getOrDefault(mPropertyName, (long) 0);
-        }
+        NonceHandler.Stats stats = mNonce.getStats();
 
         synchronized (mLock) {
-            pw.println(TextUtils.formatSimple("  Cache Name: %s", cacheName()));
-            pw.println(TextUtils.formatSimple("    Property: %s", mPropertyName));
+            pw.println(formatSimple("  Cache Name: %s", cacheName()));
+            pw.println(formatSimple("    Property: %s", mPropertyName));
             final long skips = mSkips[NONCE_CORKED] + mSkips[NONCE_UNSET] + mSkips[NONCE_DISABLED]
                     + mSkips[NONCE_BYPASS];
-            pw.println(TextUtils.formatSimple(
+            pw.println(formatSimple(
                     "    Hits: %d, Misses: %d, Skips: %d, Clears: %d",
                     mHits, mMisses, skips, mClears));
-            pw.println(TextUtils.formatSimple(
+            pw.println(formatSimple(
                     "    Skip-corked: %d, Skip-unset: %d, Skip-bypass: %d, Skip-other: %d",
                     mSkips[NONCE_CORKED], mSkips[NONCE_UNSET],
                     mSkips[NONCE_BYPASS], mSkips[NONCE_DISABLED]));
-            pw.println(TextUtils.formatSimple(
+            pw.println(formatSimple(
                     "    Nonce: 0x%016x, Invalidates: %d, CorkedInvalidates: %d",
-                    mLastSeenNonce, invalidateCount, corkedInvalidates));
-            pw.println(TextUtils.formatSimple(
+                    mLastSeenNonce, stats.invalidated, stats.corkedInvalidates));
+            pw.println(formatSimple(
                     "    Current Size: %d, Max Size: %d, HW Mark: %d, Overflows: %d",
                     mCache.size(), mMaxEntries, mHighWaterMark, mMissOverflow));
-            pw.println(TextUtils.formatSimple("    Enabled: %s", mDisabled ? "false" : "true"));
+            pw.println(formatSimple("    Enabled: %s", mDisabled ? "false" : "true"));
             pw.println("");
 
             // No specific cache was requested.  This is the default, and no details
@@ -1404,23 +1577,7 @@
                 String key = Objects.toString(entry.getKey());
                 String value = Objects.toString(entry.getValue());
 
-                pw.println(TextUtils.formatSimple("      Key: %s\n      Value: %s\n", key, value));
-            }
-        }
-    }
-
-    /**
-     * Dump the corking status.
-     */
-    @GuardedBy("sCorkLock")
-    private static void dumpCorkInfo(PrintWriter pw) {
-        ArrayList<Map.Entry<String, Integer>> activeCorks = getActiveCorks();
-        if (activeCorks.size() > 0) {
-            pw.println("  Corking Status:");
-            for (int i = 0; i < activeCorks.size(); i++) {
-                Map.Entry<String, Integer> entry = activeCorks.get(i);
-                pw.println(TextUtils.formatSimple("    Property Name: %s Count: %d",
-                                entry.getKey(), entry.getValue()));
+                pw.println(formatSimple("      Key: %s\n      Value: %s\n", key, value));
             }
         }
     }
@@ -1441,14 +1598,7 @@
         // then only that cache is reported.
         boolean detail = anyDetailed(args);
 
-        ArrayList<PropertyInvalidatedCache> activeCaches;
-        synchronized (sGlobalLock) {
-            activeCaches = getActiveCaches();
-            if (!detail) {
-                dumpCorkInfo(pw);
-            }
-        }
-
+        ArrayList<PropertyInvalidatedCache> activeCaches = getActiveCaches();
         for (int i = 0; i < activeCaches.size(); i++) {
             PropertyInvalidatedCache currentCache = activeCaches.get(i);
             currentCache.dumpContents(pw, detail, args);
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index cc90ce5..bd26db5 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -236,6 +236,7 @@
 import android.security.advancedprotection.IAdvancedProtectionService;
 import android.security.attestationverification.AttestationVerificationManager;
 import android.security.attestationverification.IAttestationVerificationManagerService;
+import android.security.keystore.KeyStoreManager;
 import android.service.oemlock.IOemLockService;
 import android.service.oemlock.OemLockManager;
 import android.service.persistentdata.IPersistentDataBlockService;
@@ -1705,6 +1706,17 @@
                     }
                 });
 
+        registerService(Context.KEYSTORE_SERVICE, KeyStoreManager.class,
+                new StaticServiceFetcher<KeyStoreManager>() {
+                    @Override
+                    public KeyStoreManager createService()
+                            throws ServiceNotFoundException {
+                        if (!android.security.Flags.keystoreGrantApi()) {
+                            throw new ServiceNotFoundException("KeyStoreManager is not supported");
+                        }
+                        return KeyStoreManager.getInstance();
+                    }});
+
         registerService(Context.CONTACT_KEYS_SERVICE, E2eeContactKeysManager.class,
                 new CachedServiceFetcher<E2eeContactKeysManager>() {
                     @Override
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index ffa3375..07106e8 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4770,6 +4770,18 @@
 
     /**
      * Use with {@link #getSystemService(String)} to retrieve a {@link
+     * android.security.keystore.KeyStoreManager} for accessing
+     * <a href="/privacy-and-security/keystore">Android Keystore</a>
+     * functions.
+     *
+     * @see #getSystemService(String)
+     * @see android.security.keystore.KeyStoreManager
+     */
+    @FlaggedApi(android.security.Flags.FLAG_KEYSTORE_GRANT_API)
+    public static final String KEYSTORE_SERVICE = "keystore";
+
+    /**
+     * Use with {@link #getSystemService(String)} to retrieve a {@link
      * android.os.storage.StorageManager} for accessing system storage
      * functions.
      *
diff --git a/core/java/android/content/OWNERS b/core/java/android/content/OWNERS
index 6d9dc45..392f62a 100644
--- a/core/java/android/content/OWNERS
+++ b/core/java/android/content/OWNERS
@@ -1,11 +1,11 @@
 # Remain no owner because multiple modules may touch this file.
 per-file Context.java = *
 per-file ContextWrapper.java = *
-per-file *Content* = file:/services/core/java/com/android/server/am/OWNERS
-per-file *Sync* = file:/services/core/java/com/android/server/am/OWNERS
+per-file *Content* = varunshah@google.com, yamasani@google.com
+per-file *Sync* = file:/apex/jobscheduler/JOB_OWNERS
 per-file IntentFilter.java = file:/PACKAGE_MANAGER_OWNERS
 per-file UriRelativeFilter* = file:/PACKAGE_MANAGER_OWNERS
-per-file IntentFilter.java = file:/services/core/java/com/android/server/am/OWNERS
+per-file IntentFilter.java = file:/INTENT_OWNERS
 per-file Intent.java = file:/INTENT_OWNERS
 per-file AutofillOptions* = file:/core/java/android/service/autofill/OWNERS
 per-file ContentCaptureOptions* = file:/core/java/android/service/contentcapture/OWNERS
diff --git a/core/java/android/net/vcn/flags.aconfig b/core/java/android/net/vcn/flags.aconfig
index efddd1f..5b30624 100644
--- a/core/java/android/net/vcn/flags.aconfig
+++ b/core/java/android/net/vcn/flags.aconfig
@@ -14,11 +14,4 @@
     namespace: "vcn"
     description: "Feature flag for adjustable safe mode timeout"
     bug: "317406085"
-}
-
-flag{
-    name: "network_metric_monitor"
-    namespace: "vcn"
-    description: "Feature flag for enabling network metric monitor"
-    bug: "282996138"
 }
\ No newline at end of file
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index a698b9d..ddcb5c6 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -1031,7 +1031,10 @@
             return this;
         }
 
-        private long getStatsDuration() {
+        /**
+         * Returns the duration of the battery session reflected by these stats.
+         */
+        public long getStatsDuration() {
             if (mStatsDurationMs != -1) {
                 return mStatsDurationMs;
             } else {
diff --git a/core/java/android/os/BatteryUsageStatsQuery.java b/core/java/android/os/BatteryUsageStatsQuery.java
index b533225..e68c4ca 100644
--- a/core/java/android/os/BatteryUsageStatsQuery.java
+++ b/core/java/android/os/BatteryUsageStatsQuery.java
@@ -20,6 +20,8 @@
 import android.annotation.NonNull;
 import android.util.IntArray;
 
+import com.android.internal.os.MonotonicClock;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -85,8 +87,11 @@
     @NonNull
     private final int[] mUserIds;
     private final long mMaxStatsAgeMs;
-    private final long mFromTimestamp;
-    private final long mToTimestamp;
+
+    private final long mAggregatedFromTimestamp;
+    private final long mAggregatedToTimestamp;
+    private long mMonotonicStartTime;
+    private long mMonotonicEndTime;
     private final double mMinConsumedPowerThreshold;
     private final @BatteryConsumer.PowerComponentId int[] mPowerComponents;
 
@@ -96,8 +101,10 @@
                 : new int[]{UserHandle.USER_ALL};
         mMaxStatsAgeMs = builder.mMaxStatsAgeMs;
         mMinConsumedPowerThreshold = builder.mMinConsumedPowerThreshold;
-        mFromTimestamp = builder.mFromTimestamp;
-        mToTimestamp = builder.mToTimestamp;
+        mAggregatedFromTimestamp = builder.mAggregateFromTimestamp;
+        mAggregatedToTimestamp = builder.mAggregateToTimestamp;
+        mMonotonicStartTime = builder.mMonotonicStartTime;
+        mMonotonicEndTime = builder.mMonotonicEndTime;
         mPowerComponents = builder.mPowerComponents;
     }
 
@@ -163,11 +170,27 @@
     }
 
     /**
-     * Returns the exclusive lower bound of the stored snapshot timestamps that should be included
-     * in the aggregation.  Ignored if {@link #getToTimestamp()} is zero.
+     * Returns the exclusive lower bound of the battery history that should be included in
+     * the aggregated battery usage stats.
      */
-    public long getFromTimestamp() {
-        return mFromTimestamp;
+    public long getMonotonicStartTime() {
+        return mMonotonicStartTime;
+    }
+
+    /**
+     * Returns the inclusive upper bound of the battery history that should be included in
+     * the aggregated battery usage stats.
+     */
+    public long getMonotonicEndTime() {
+        return mMonotonicEndTime;
+    }
+
+    /**
+     * Returns the exclusive lower bound of the stored snapshot timestamps that should be included
+     * in the aggregation.  Ignored if {@link #getAggregatedToTimestamp()} is zero.
+     */
+    public long getAggregatedFromTimestamp() {
+        return mAggregatedFromTimestamp;
     }
 
     /**
@@ -175,8 +198,8 @@
      * be included in the aggregation.  The default is to include only the current stats
      * accumulated since the latest battery reset.
      */
-    public long getToTimestamp() {
-        return mToTimestamp;
+    public long getAggregatedToTimestamp() {
+        return mAggregatedToTimestamp;
     }
 
     private BatteryUsageStatsQuery(Parcel in) {
@@ -185,8 +208,8 @@
         in.readIntArray(mUserIds);
         mMaxStatsAgeMs = in.readLong();
         mMinConsumedPowerThreshold = in.readDouble();
-        mFromTimestamp = in.readLong();
-        mToTimestamp = in.readLong();
+        mAggregatedFromTimestamp = in.readLong();
+        mAggregatedToTimestamp = in.readLong();
         mPowerComponents = in.createIntArray();
     }
 
@@ -197,8 +220,8 @@
         dest.writeIntArray(mUserIds);
         dest.writeLong(mMaxStatsAgeMs);
         dest.writeDouble(mMinConsumedPowerThreshold);
-        dest.writeLong(mFromTimestamp);
-        dest.writeLong(mToTimestamp);
+        dest.writeLong(mAggregatedFromTimestamp);
+        dest.writeLong(mAggregatedToTimestamp);
         dest.writeIntArray(mPowerComponents);
     }
 
@@ -228,8 +251,10 @@
         private int mFlags;
         private IntArray mUserIds;
         private long mMaxStatsAgeMs = DEFAULT_MAX_STATS_AGE_MS;
-        private long mFromTimestamp;
-        private long mToTimestamp;
+        private long mMonotonicStartTime = MonotonicClock.UNDEFINED;
+        private long mMonotonicEndTime = MonotonicClock.UNDEFINED;
+        private long mAggregateFromTimestamp;
+        private long mAggregateToTimestamp;
         private double mMinConsumedPowerThreshold = 0;
         private @BatteryConsumer.PowerComponentId int[] mPowerComponents;
 
@@ -241,6 +266,17 @@
         }
 
         /**
+         * Specifies the time range for the requested stats, in terms of MonotonicClock
+         * @param monotonicStartTime Inclusive starting monotonic timestamp
+         * @param monotonicEndTime Exclusive ending timestamp. Can be MonotonicClock.UNDEFINED
+         */
+        public Builder monotonicTimeRange(long monotonicStartTime, long monotonicEndTime) {
+            mMonotonicStartTime = monotonicStartTime;
+            mMonotonicEndTime = monotonicEndTime;
+            return this;
+        }
+
+        /**
          * Add a user whose battery stats should be included in the battery usage stats.
          * {@link UserHandle#USER_ALL} will be used by default if no users are added explicitly.
          */
@@ -345,8 +381,8 @@
          */
         // TODO(b/298459065): switch to monotonic clock
         public Builder aggregateSnapshots(long fromTimestamp, long toTimestamp) {
-            mFromTimestamp = fromTimestamp;
-            mToTimestamp = toTimestamp;
+            mAggregateFromTimestamp = fromTimestamp;
+            mAggregateToTimestamp = toTimestamp;
             return this;
         }
 
diff --git a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
index b2d9260..6afb8e0 100644
--- a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
@@ -754,7 +754,7 @@
                 // Idle handles only run if the queue is empty or if the first message
                 // in the queue (possibly a barrier) is due to be handled in the future.
                 if (pendingIdleHandlerCount < 0
-                        && mNextPollTimeoutMillis != 0) {
+                        && isIdle()) {
                     pendingIdleHandlerCount = mIdleHandlers.size();
                 }
                 if (pendingIdleHandlerCount <= 0) {
diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS
index a1b75034..590ddb4 100644
--- a/core/java/android/os/OWNERS
+++ b/core/java/android/os/OWNERS
@@ -122,3 +122,6 @@
 per-file StatsBootstrapAtomValue.aidl = file:/services/core/java/com/android/server/stats/OWNERS
 per-file StatsBootstrapAtomService.java = file:/services/core/java/com/android/server/stats/OWNERS
 per-file StatsServiceManager.java = file:/services/core/java/com/android/server/stats/OWNERS
+
+# Dropbox
+per-file DropBoxManager* = mwachens@google.com
diff --git a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
index 80c24a9..02335972 100644
--- a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
@@ -712,7 +712,7 @@
                 // Idle handles only run if the queue is empty or if the first message
                 // in the queue (possibly a barrier) is due to be handled in the future.
                 if (pendingIdleHandlerCount < 0
-                        && mNextPollTimeoutMillis != 0) {
+                        && isIdle()) {
                     pendingIdleHandlerCount = mIdleHandlers.size();
                 }
                 if (pendingIdleHandlerCount <= 0) {
diff --git a/core/java/android/os/UidBatteryConsumer.java b/core/java/android/os/UidBatteryConsumer.java
index 7f7ef04..f893739 100644
--- a/core/java/android/os/UidBatteryConsumer.java
+++ b/core/java/android/os/UidBatteryConsumer.java
@@ -109,9 +109,11 @@
      * Returns the amount of time in milliseconds this UID spent in the specified process state.
      */
     public long getTimeInProcessStateMs(@ProcessState int state) {
-        Key key = getKey(POWER_COMPONENT_BASE, state);
-        if (key != null) {
-            return getUsageDurationMillis(key);
+        if (state != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
+            Key key = getKey(POWER_COMPONENT_BASE, state);
+            if (key != null) {
+                return getUsageDurationMillis(key);
+            }
         }
         return 0;
     }
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index acf4a2f..fa99f35 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -5602,14 +5602,30 @@
             android.Manifest.permission.MANAGE_USERS,
             android.Manifest.permission.INTERACT_ACROSS_USERS
     })
+    @CachedProperty(api = "user_manager_users")
     public @Nullable UserHandle getProfileParent(@NonNull UserHandle user) {
-        UserInfo info = getProfileParent(user.getIdentifier());
-
-        if (info == null) {
-            return null;
+        if (android.multiuser.Flags.cacheProfileParentReadOnly()) {
+            final UserHandle userHandle = UserManagerCache.getProfileParent(
+                    (UserHandle query) -> {
+                        UserInfo info = getProfileParent(query.getIdentifier());
+                        // TODO: Remove when b/372923336 is fixed
+                        if (info == null) {
+                            return UserHandle.of(UserHandle.USER_NULL);
+                        }
+                        return UserHandle.of(info.id);
+                    },
+                    user);
+            if (userHandle.getIdentifier() == UserHandle.USER_NULL) {
+                return null;
+            }
+            return userHandle;
+        } else {
+            UserInfo info = getProfileParent(user.getIdentifier());
+            if (info == null) {
+                return null;
+            }
+            return UserHandle.of(info.id);
         }
-
-        return UserHandle.of(info.id);
     }
 
     /**
@@ -6424,6 +6440,9 @@
      */
     public static final void invalidateCacheOnUserListChange() {
         UserManagerCache.invalidateUserSerialNumber();
+        if (android.multiuser.Flags.cacheProfileParentReadOnly()) {
+            UserManagerCache.invalidateProfileParent();
+        }
     }
 
     /**
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index aedf8e0..1d35344 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -113,3 +113,10 @@
     description: "AFL feature"
     bug: "365994454"
 }
+
+flag {
+    name: "keystore_grant_api"
+    namespace: "hardware_backed_security"
+    description: "Feature flag for exposing KeyStore grant APIs"
+    bug: "351158708"
+}
diff --git a/core/java/android/telephony/TelephonyCallback.java b/core/java/android/telephony/TelephonyCallback.java
index 14d5800..5295b60 100644
--- a/core/java/android/telephony/TelephonyCallback.java
+++ b/core/java/android/telephony/TelephonyCallback.java
@@ -33,6 +33,7 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.WeaklyReferencedCallback;
 import com.android.internal.telephony.IPhoneStateListener;
 import com.android.internal.telephony.flags.Flags;
 
@@ -70,6 +71,7 @@
  * its manifest file. Where permissions apply, they are noted in the
  * appropriate sub-interfaces.
  */
+@WeaklyReferencedCallback
 public class TelephonyCallback {
     private static final String LOG_TAG = "TelephonyCallback";
     /**
diff --git a/core/java/android/view/DisplayEventReceiver.java b/core/java/android/view/DisplayEventReceiver.java
index 18080e4..fc7a65d 100644
--- a/core/java/android/view/DisplayEventReceiver.java
+++ b/core/java/android/view/DisplayEventReceiver.java
@@ -24,6 +24,7 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.WeaklyReferencedCallback;
 
 import dalvik.annotation.optimization.FastNative;
 
@@ -40,6 +41,7 @@
  *
  * @hide
  */
+@WeaklyReferencedCallback
 public abstract class DisplayEventReceiver {
 
     /**
diff --git a/core/java/android/view/LetterboxScrollProcessor.java b/core/java/android/view/LetterboxScrollProcessor.java
index dc736d6..1364a82 100644
--- a/core/java/android/view/LetterboxScrollProcessor.java
+++ b/core/java/android/view/LetterboxScrollProcessor.java
@@ -16,6 +16,8 @@
 
 package android.view;
 
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Rect;
@@ -23,6 +25,8 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -35,6 +39,7 @@
  *
  * @hide
  */
+@VisibleForTesting(visibility = PACKAGE)
 public class LetterboxScrollProcessor {
 
     private enum LetterboxScrollState {
@@ -53,6 +58,7 @@
     /** IDs of events generated from this class */
     private final Set<Integer> mGeneratedEventIds = new HashSet<>();
 
+    @VisibleForTesting(visibility = PACKAGE)
     public LetterboxScrollProcessor(@NonNull Context context, @Nullable Handler handler) {
         mContext = context;
         mScrollDetector = new GestureDetector(context, new ScrollListener(), handler);
@@ -69,7 +75,9 @@
      * @return The list of adjusted events, or null if no adjustments are needed. The list is empty
      * if the event should be ignored. Do not keep a reference to the output as the list is reused.
      */
-    public List<MotionEvent> processMotionEvent(MotionEvent motionEvent) {
+    @Nullable
+    @VisibleForTesting(visibility = PACKAGE)
+    public List<MotionEvent> processMotionEvent(@NonNull MotionEvent motionEvent) {
         mProcessedEvents.clear();
         final Rect appBounds = getAppBounds();
 
@@ -124,11 +132,9 @@
             mState = LetterboxScrollState.AWAITING_GESTURE_START;
         }
 
-        if (makeNoAdjustments) return null;
-        return mProcessedEvents;
+        return makeNoAdjustments ? null : mProcessedEvents;
     }
 
-
     /**
      * Processes the InputEvent for compatibility before it is finished by calling
      * InputEventReceiver#finishInputEvent().
@@ -136,21 +142,33 @@
      * @param motionEvent The MotionEvent to process.
      * @return The motionEvent to finish, or null if it should not be finished.
      */
-    public InputEvent processMotionEventBeforeFinish(MotionEvent motionEvent) {
-        if (mGeneratedEventIds.remove(motionEvent.getId())) return null;
-        return motionEvent;
+    @Nullable
+    @VisibleForTesting(visibility = PACKAGE)
+    public InputEvent processMotionEventBeforeFinish(@NonNull MotionEvent motionEvent) {
+        return mGeneratedEventIds.remove(motionEvent.getId()) ? null : motionEvent;
     }
 
+    @NonNull
     private Rect getAppBounds() {
         return mContext.getResources().getConfiguration().windowConfiguration.getBounds();
     }
 
-    private boolean isOutsideAppBounds(MotionEvent motionEvent, Rect appBounds) {
-        return motionEvent.getX() < 0 || motionEvent.getX() >= appBounds.width()
-                || motionEvent.getY() < 0 || motionEvent.getY() >= appBounds.height();
+    /** Checks whether the gesture is located on the letterbox area. */
+    private boolean isOutsideAppBounds(@NonNull MotionEvent motionEvent, @NonNull Rect appBounds) {
+        // The events are in the coordinate system of the ViewRootImpl (window). The window might
+        // not have the same dimensions as the app bounds - for example in case of Dialogs - thus
+        // `getRawX()` and `getRawY()` are used, with the absolute bounds (left, top, etc) instead
+        // of width and height.
+        // The event should be passed to the app if it has happened anywhere in the app area,
+        // irrespective of the current window size, therefore the app bounds are used instead of the
+        // current window.
+        return motionEvent.getRawX() < appBounds.left
+                || motionEvent.getRawX() >= appBounds.right
+                || motionEvent.getRawY() < appBounds.top
+                || motionEvent.getRawY() >= appBounds.bottom;
     }
 
-    private void applyOffset(MotionEvent event, Rect appBounds) {
+    private void applyOffset(@NonNull MotionEvent event, @NonNull Rect appBounds) {
         float horizontalOffset = calculateOffset(event.getX(), appBounds.width());
         float verticalOffset = calculateOffset(event.getY(), appBounds.height());
         // Apply the offset to the motion event so it is over the app's view.
diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS
index 1ea58bc..80484a6 100644
--- a/core/java/android/view/OWNERS
+++ b/core/java/android/view/OWNERS
@@ -88,6 +88,7 @@
 per-file OnReceiveContentListener.java = file:/core/java/android/widget/OWNERS
 per-file ContentInfo.java = file:/core/java/android/service/autofill/OWNERS
 per-file ContentInfo.java = file:/core/java/android/widget/OWNERS
+per-file view_flags.aconfig = file:/services/core/java/com/android/server/wm/OWNERS
 
 # WindowManager
 per-file ContentRecordingSession.aidl = file:/services/core/java/com/android/server/wm/OWNERS
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 82235d2..9cad3e5 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -16,6 +16,7 @@
 
 package android.view;
 
+import static android.view.flags.Flags.FLAG_SURFACE_VIEW_GET_SURFACE_PACKAGE;
 import static android.view.flags.Flags.FLAG_SURFACE_VIEW_SET_COMPOSITION_ORDER;
 import static android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
 import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_OVERLAY_SUBLAYER;
@@ -27,6 +28,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.res.CompatibilityInfo.Translator;
@@ -2112,7 +2114,7 @@
     }
 
     /**
-     * Display the view-hierarchy embedded within a {@link SurfaceControlViewHost.SurfacePackage}
+     * Displays the view-hierarchy embedded within a {@link SurfaceControlViewHost.SurfacePackage}
      * within this SurfaceView.
      *
      * This can be called independently of the SurfaceView lifetime callbacks. SurfaceView
@@ -2132,6 +2134,8 @@
      * SurfaceView the underlying {@link SurfaceControlViewHost} remains managed by it's original
      * remote-owner.
      *
+     * Users can call {@link SurfaceView#clearChildSurfacePackage} to clear the package.
+     *
      * @param p The SurfacePackage to embed.
      */
     public void setChildSurfacePackage(@NonNull SurfaceControlViewHost.SurfacePackage p) {
@@ -2155,6 +2159,46 @@
         invalidate();
     }
 
+    /**
+     * Returns the {@link SurfaceControlViewHost.SurfacePackage} that was set on this SurfaceView.
+     *
+     * Note: This method will return {@code null} if
+     * {@link #setChildSurfacePackage(SurfaceControlViewHost.SurfacePackage)}
+     * has not been called or if {@link #clearChildSurfacePackage()} has been called.
+     *
+     * @see #setChildSurfacePackage(SurfaceControlViewHost.SurfacePackage)
+     */
+    @SuppressLint("GetterSetterNullability")
+    @FlaggedApi(FLAG_SURFACE_VIEW_GET_SURFACE_PACKAGE)
+    public @Nullable SurfaceControlViewHost.SurfacePackage getChildSurfacePackage() {
+        return mSurfacePackage;
+    }
+
+    /**
+     * Clears the {@link SurfaceControlViewHost.SurfacePackage} that was set on this SurfaceView.
+     * This hides any content rendered by the provided
+     * {@link SurfaceControlViewHost.SurfacePackage}.
+     *
+     * @see #setChildSurfacePackage(SurfaceControlViewHost.SurfacePackage)
+     */
+    @FlaggedApi(FLAG_SURFACE_VIEW_GET_SURFACE_PACKAGE)
+    public void clearChildSurfacePackage() {
+        if (mSurfacePackage != null) {
+            mSurfaceControlViewHostParent.detach();
+            mEmbeddedWindowParams.clear();
+
+            // Reparent the SurfaceControl to remove the content on screen.
+            final SurfaceControl sc = mSurfacePackage.getSurfaceControl();
+            final SurfaceControl.Transaction transaction = new Transaction();
+            transaction.reparent(sc, null);
+            mSurfacePackage.release();
+            applyTransactionOnVriDraw(transaction);
+
+            mSurfacePackage = null;
+            invalidate();
+        }
+    }
+
     private void reparentSurfacePackage(SurfaceControl.Transaction t,
             SurfaceControlViewHost.SurfacePackage p) {
         final SurfaceControl sc = p.getSurfaceControl();
diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig
index bb61ae4..1b86f96 100644
--- a/core/java/android/view/flags/view_flags.aconfig
+++ b/core/java/android/view/flags/view_flags.aconfig
@@ -119,6 +119,14 @@
 }
 
 flag {
+    name: "surface_view_get_surface_package"
+    namespace: "window_surfaces"
+    description: "Add APIs to manage SurfacePackage of the parent SurfaceView."
+    bug: "341021569"
+    is_fixed_read_only: true
+}
+
+flag {
     name: "use_refactored_round_scrollbar"
     namespace: "wear_frameworks"
     description: "Use refactored round scrollbar."
diff --git a/core/java/android/window/BackNavigationInfo.java b/core/java/android/window/BackNavigationInfo.java
index 59639d0..6cefc4d 100644
--- a/core/java/android/window/BackNavigationInfo.java
+++ b/core/java/android/window/BackNavigationInfo.java
@@ -16,6 +16,8 @@
 
 package android.window;
 
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+
 import android.annotation.AnimRes;
 import android.annotation.ColorInt;
 import android.annotation.IntDef;
@@ -118,6 +120,7 @@
     private final Rect mTouchableRegion;
 
     private final boolean mAppProgressGenerationAllowed;
+    private final int mFocusedTaskId;
 
     /**
      * Create a new {@link BackNavigationInfo} instance.
@@ -135,7 +138,8 @@
             @Nullable CustomAnimationInfo customAnimationInfo,
             int letterboxColor,
             @Nullable Rect touchableRegion,
-            boolean appProgressGenerationAllowed) {
+            boolean appProgressGenerationAllowed,
+            int focusedTaskId) {
         mType = type;
         mOnBackNavigationDone = onBackNavigationDone;
         mOnBackInvokedCallback = onBackInvokedCallback;
@@ -145,6 +149,7 @@
         mLetterboxColor = letterboxColor;
         mTouchableRegion = new Rect(touchableRegion);
         mAppProgressGenerationAllowed = appProgressGenerationAllowed;
+        mFocusedTaskId = focusedTaskId;
     }
 
     private BackNavigationInfo(@NonNull Parcel in) {
@@ -157,6 +162,7 @@
         mLetterboxColor = in.readInt();
         mTouchableRegion = in.readTypedObject(Rect.CREATOR);
         mAppProgressGenerationAllowed = in.readBoolean();
+        mFocusedTaskId = in.readInt();
     }
 
     /** @hide */
@@ -171,6 +177,7 @@
         dest.writeInt(mLetterboxColor);
         dest.writeTypedObject(mTouchableRegion, flags);
         dest.writeBoolean(mAppProgressGenerationAllowed);
+        dest.writeInt(mFocusedTaskId);
     }
 
     /**
@@ -238,6 +245,14 @@
     }
 
     /**
+     * @return The focused task id when back gesture start.
+     * @hide
+     */
+    public int getFocusedTaskId() {
+        return mFocusedTaskId;
+    }
+
+    /**
      * Callback to be called when the back preview is finished in order to notify the server that
      * it can clean up the resources created for the animation.
      * @hide
@@ -435,6 +450,7 @@
         private int mLetterboxColor = Color.TRANSPARENT;
         private Rect mTouchableRegion;
         private boolean mAppProgressGenerationAllowed;
+        private int mFocusedTaskId = INVALID_TASK_ID;
 
         /**
          * @see BackNavigationInfo#getType()
@@ -527,6 +543,14 @@
         }
 
         /**
+         * @param focusedTaskId The current focused taskId when back gesture start.
+         */
+        public Builder setFocusedTaskId(int focusedTaskId) {
+            mFocusedTaskId = focusedTaskId;
+            return this;
+        }
+
+        /**
          * Builds and returns an instance of {@link BackNavigationInfo}
          */
         public BackNavigationInfo build() {
@@ -537,7 +561,8 @@
                     mCustomAnimationInfo,
                     mLetterboxColor,
                     mTouchableRegion,
-                    mAppProgressGenerationAllowed);
+                    mAppProgressGenerationAllowed,
+                    mFocusedTaskId);
         }
     }
 }
diff --git a/core/java/android/window/SnapshotDrawerUtils.java b/core/java/android/window/SnapshotDrawerUtils.java
index 9a7bce0..5397da1 100644
--- a/core/java/android/window/SnapshotDrawerUtils.java
+++ b/core/java/android/window/SnapshotDrawerUtils.java
@@ -151,7 +151,9 @@
         @VisibleForTesting
         public void setFrames(Rect frame, Rect systemBarInsets) {
             mFrame.set(frame);
-            mSizeMismatch = (mFrame.width() != mSnapshotW || mFrame.height() != mSnapshotH);
+            final Rect letterboxInsets = mSnapshot.getLetterboxInsets();
+            mSizeMismatch = (mFrame.width() != mSnapshotW || mFrame.height() != mSnapshotH)
+                    || letterboxInsets.left != 0 || letterboxInsets.top != 0;
             if (!Flags.drawSnapshotAspectRatioMatch() && systemBarInsets != null) {
                 mSystemBarInsets.set(systemBarInsets);
                 mSystemBarBackgroundPainter.setInsets(systemBarInsets);
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 0d235ff..d15f52c 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -9,6 +9,16 @@
 }
 
 flag {
+  name: "reset_draw_state_on_client_invisible"
+  namespace: "windowing_frontend"
+  description: "Reset draw state if the client is notified to be invisible"
+  bug: "373023636"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
     name: "wait_for_transition_on_display_switch"
     namespace: "windowing_frontend"
     description: "Waits for Shell transition to start before unblocking the screen after display switch"
diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java
index d474c6d..6448f10 100644
--- a/core/java/com/android/internal/jank/FrameTracker.java
+++ b/core/java/com/android/internal/jank/FrameTracker.java
@@ -30,6 +30,7 @@
 import static com.android.internal.jank.InteractionJankMonitor.ACTION_SESSION_END;
 import static com.android.internal.jank.InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT;
 
+import android.animation.AnimationHandler;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -344,7 +345,8 @@
     @UiThread
     public boolean end(@Reasons int reason) {
         if (mCancelled || mEndVsyncId != INVALID_ID) return false;
-        mEndVsyncId = mChoreographer.getVsyncId();
+        mEndVsyncId = AnimationHandler.getInstance().getLastAnimationFrameVsyncId(
+                mChoreographer.getVsyncId());
         // Cancel the session if:
         // 1. The session begins and ends at the same vsync id.
         // 2. The session never begun.
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index d61785e..07aa720 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -83,7 +83,7 @@
     private static final String TAG = "BatteryStatsHistory";
 
     // Current on-disk Parcel version. Must be updated when the format of the parcelable changes
-    private static final int VERSION = 210;
+    private static final int VERSION = 211;
 
     private static final String HISTORY_DIR = "battery-history";
     private static final String FILE_SUFFIX = ".bh";
@@ -210,6 +210,8 @@
     private final MonotonicClock mMonotonicClock;
     // Monotonic time when we started writing to the history buffer
     private long mHistoryBufferStartTime;
+    // Monotonically increasing size of written history
+    private long mMonotonicHistorySize;
     private final ArraySet<PowerStats.Descriptor> mWrittenPowerStatsDescriptors = new ArraySet<>();
     private byte mLastHistoryStepLevel = 0;
     private boolean mMutable = true;
@@ -909,6 +911,8 @@
                 }
                 // skip monotonic time field.
                 p.readLong();
+                // skip monotonic size field
+                p.readLong();
 
                 final int bufSize = p.readInt();
                 final int curPos = p.dataPosition();
@@ -964,6 +968,8 @@
         }
         // skip monotonic time field.
         out.readLong();
+        // skip monotonic size field
+        out.readLong();
         return true;
     }
 
@@ -987,6 +993,7 @@
         p.setDataPosition(0);
         p.readInt();        // Skip the version field
         long monotonicTime = p.readLong();
+        p.readLong();       // Skip monotonic size field
         p.setDataPosition(pos);
         return monotonicTime;
     }
@@ -1819,6 +1826,7 @@
             // as long as no bit has changed both between now and the last entry, as
             // well as the last entry and the one before it (so we capture any toggles).
             if (DEBUG) Slog.i(TAG, "ADD: rewinding back to " + mHistoryBufferLastPos);
+            mMonotonicHistorySize -= (mHistoryBuffer.dataSize() - mHistoryBufferLastPos);
             mHistoryBuffer.setDataSize(mHistoryBufferLastPos);
             mHistoryBuffer.setDataPosition(mHistoryBufferLastPos);
             mHistoryBufferLastPos = -1;
@@ -1934,6 +1942,7 @@
         }
         mHistoryLastWritten.tagsFirstOccurrence = hasTags;
         writeHistoryDelta(mHistoryBuffer, mHistoryLastWritten, mHistoryLastLastWritten);
+        mMonotonicHistorySize += (mHistoryBuffer.dataSize() - mHistoryBufferLastPos);
         cur.wakelockTag = null;
         cur.wakeReasonTag = null;
         cur.eventCode = HistoryItem.EVENT_NONE;
@@ -2344,6 +2353,8 @@
             }
 
             mHistoryBufferStartTime = in.readLong();
+            mMonotonicHistorySize = in.readLong();
+
             mHistoryBuffer.setDataSize(0);
             mHistoryBuffer.setDataPosition(0);
 
@@ -2370,6 +2381,7 @@
     private void writeHistoryBuffer(Parcel out) {
         out.writeInt(BatteryStatsHistory.VERSION);
         out.writeLong(mHistoryBufferStartTime);
+        out.writeLong(mMonotonicHistorySize);
         out.writeInt(mHistoryBuffer.dataSize());
         if (DEBUG) {
             Slog.i(TAG, "***************** WRITING HISTORY: "
@@ -2457,6 +2469,14 @@
     }
 
     /**
+     * Returns the monotonically increasing size of written history, including the buffers
+     * that have already been discarded.
+     */
+    public long getMonotonicHistorySize() {
+        return mMonotonicHistorySize;
+    }
+
+    /**
      * Prints battery stats history for debugging.
      */
     public void dump(PrintWriter pw, long startTimeMs, long endTimeMs) {
diff --git a/core/proto/android/app/OWNERS b/core/proto/android/app/OWNERS
index a137ea9..519bf9a 100644
--- a/core/proto/android/app/OWNERS
+++ b/core/proto/android/app/OWNERS
@@ -1,3 +1,3 @@
-per-file appstartinfo.proto = file:/services/core/java/com/android/server/am/OWNERS
+per-file appstartinfo.proto = file:/PERFORMANCE_OWNERS
 per-file location_time_zone_manager.proto = file:platform/frameworks/base:/services/core/java/com/android/server/timezonedetector/OWNERS
 per-file time_zone_detector.proto = file:platform/frameworks/base:/services/core/java/com/android/server/timezonedetector/OWNERS
diff --git a/core/res/OWNERS b/core/res/OWNERS
index d109cee..faed4d8 100644
--- a/core/res/OWNERS
+++ b/core/res/OWNERS
@@ -53,7 +53,7 @@
 per-file res/values/dimens_car.xml = file:/platform/packages/services/Car:/OWNERS
 
 # Device Idle
-per-file res/values/config_device_idle.xml = file:/apex/jobscheduler/OWNERS
+per-file res/values/config_device_idle.xml = file:/apex/jobscheduler/DEVICE_IDLE_OWNERS
 
 # Display Manager
 per-file res/values/config_display.xml = file:/services/core/java/com/android/server/display/OWNERS
diff --git a/core/res/res/values/config_battery_stats.xml b/core/res/res/values/config_battery_stats.xml
index 80cf088..9498273 100644
--- a/core/res/res/values/config_battery_stats.xml
+++ b/core/res/res/values/config_battery_stats.xml
@@ -45,7 +45,12 @@
     <integer name="config_powerStatsAggregationPeriod">14400000</integer>
 
     <!-- PowerStats aggregation span duration in milliseconds. This is the length of battery
-    history time for every aggregated power stats span that is stored stored in PowerStatsStore.
+    history time for every aggregated power stats span that is stored in PowerStatsStore.
     It should not be larger than config_powerStatsAggregationPeriod (but it can be the same) -->
     <integer name="config_aggregatedPowerStatsSpanDuration">3600000</integer>
+
+    <!-- BatteryUsageStats accumulation period as determined by the size of accumulated
+    battery history, in bytes. -->
+    <integer name="config_accumulatedBatteryUsageStatsSpanSize">32768</integer>
+
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b424955..0b2b345 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5301,6 +5301,7 @@
   <java-symbol type="string" name="config_powerStatsThrottlePeriods" />
   <java-symbol type="integer" name="config_powerStatsAggregationPeriod" />
   <java-symbol type="integer" name="config_aggregatedPowerStatsSpanDuration" />
+  <java-symbol type="integer" name="config_accumulatedBatteryUsageStatsSpanSize" />
 
   <java-symbol name="materialColorOnSecondaryFixedVariant" type="attr"/>
   <java-symbol name="materialColorOnTertiaryFixedVariant" type="attr"/>
diff --git a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
index eb463fd..0469846 100644
--- a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
+++ b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
@@ -18,6 +18,8 @@
 
 import static android.test.MoreAsserts.assertNotEqual;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
@@ -892,6 +894,34 @@
     }
 
     @Test
+    public void testPostNotifyEndListener() throws Throwable {
+        ValueAnimator.setPostNotifyEndListenerEnabled(true);
+        final CountDownLatch latch = new CountDownLatch(1);
+        final long[] lastAnimFrameId = new long[1];
+        final long[] endAnimCallbackId = new long[1];
+        try {
+            a1.addUpdateListener(animator -> {
+                if (animator.getAnimatedFraction() == 1f) {
+                    lastAnimFrameId[0] = Choreographer.getInstance().getVsyncId();
+                }
+            });
+            a1.addListener(new MyListener() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    super.onAnimationEnd(animation);
+                    endAnimCallbackId[0] = Choreographer.getInstance().getVsyncId();
+                    latch.countDown();
+                }
+            });
+            mActivityRule.runOnUiThread(() -> a1.start());
+            assertTrue(latch.await(1, TimeUnit.SECONDS));
+            assertThat(endAnimCallbackId[0]).isGreaterThan(lastAnimFrameId[0]);
+        } finally {
+            ValueAnimator.setPostNotifyEndListenerEnabled(false);
+        }
+    }
+
+    @Test
     public void testZeroDuration() throws Throwable {
         // Run two animators with zero duration, with one running forward and the other one
         // backward. Check that the animations start and finish with the correct end fractions.
diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
index b5ee130..dcea5b2 100644
--- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
+++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
@@ -19,6 +19,8 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import android.platform.test.annotations.IgnoreUnderRavenwood;
 import android.platform.test.ravenwood.RavenwoodRule;
@@ -26,6 +28,7 @@
 import androidx.test.filters.SmallTest;
 
 import org.junit.After;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 
@@ -84,14 +87,20 @@
         public Boolean apply(Integer x) {
             return mServer.query(x);
         }
+
         @Override
         public boolean shouldBypassCache(Integer x) {
             return x % 13 == 0;
         }
     }
 
-    // Clear the test mode after every test, in case this process is used for other
-    // tests. This also resets the test property map.
+    // Prepare for testing.
+    @Before
+    public void setUp() throws Exception {
+        PropertyInvalidatedCache.setTestMode(true);
+    }
+
+    // Ensure all test configurations are cleared.
     @After
     public void tearDown() throws Exception {
         PropertyInvalidatedCache.setTestMode(false);
@@ -111,9 +120,6 @@
                 new PropertyInvalidatedCache<>(4, MODULE, API, "cache1",
                         new ServerQuery(tester));
 
-        PropertyInvalidatedCache.setTestMode(true);
-        testCache.testPropertyName();
-
         tester.verify(0);
         assertEquals(tester.value(3), testCache.query(3));
         tester.verify(1);
@@ -223,22 +229,16 @@
 
         TestCache(String module, String api) {
             this(module, api, new TestQuery());
-            setTestMode(true);
-            testPropertyName();
         }
 
         TestCache(String module, String api, TestQuery query) {
             super(4, module, api, api, query);
             mQuery = query;
-            setTestMode(true);
-            testPropertyName();
         }
 
         public int getRecomputeCount() {
             return mQuery.getRecomputeCount();
         }
-
-
     }
 
     @Test
@@ -375,4 +375,52 @@
             PropertyInvalidatedCache.MODULE_BLUETOOTH, "getState");
         assertEquals(n1, "cache_key.bluetooth.get_state");
     }
+
+    // Verify that test mode works properly.
+    @Test
+    public void testTestMode() {
+        // Create a cache that will write a system nonce.
+        TestCache sysCache = new TestCache(PropertyInvalidatedCache.MODULE_SYSTEM, "mode1");
+        try {
+            // Invalidate the cache, which writes the system property.  There must be a permission
+            // failure.
+            sysCache.invalidateCache();
+            fail("expected permission failure");
+        } catch (RuntimeException e) {
+            // The expected exception is a bare RuntimeException.  The test does not attempt to
+            // validate the text of the exception message.
+        }
+
+        sysCache.testPropertyName();
+        // Invalidate the cache.  This must succeed because the property has been marked for
+        // testing.
+        sysCache.invalidateCache();
+
+        // Create a cache that uses MODULE_TEST.  Invalidation succeeds whether or not the
+        // property is tagged as being tested.
+        TestCache testCache = new TestCache(PropertyInvalidatedCache.MODULE_TEST, "mode2");
+        testCache.invalidateCache();
+        testCache.testPropertyName();
+        testCache.invalidateCache();
+
+        // Clear test mode.  This fails if test mode is not enabled.
+        PropertyInvalidatedCache.setTestMode(false);
+        try {
+            PropertyInvalidatedCache.setTestMode(false);
+            fail("expected an IllegalStateException");
+        } catch (IllegalStateException e) {
+            // The expected exception.
+        }
+        // Configuring a property for testing must fail if test mode is false.
+        TestCache cache2 = new TestCache(PropertyInvalidatedCache.MODULE_SYSTEM, "mode3");
+        try {
+            cache2.testPropertyName();
+            fail("expected an IllegalStateException");
+        } catch (IllegalStateException e) {
+            // The expected exception.
+        }
+
+        // Re-enable test mode (so that the cleanup for the test does not throw).
+        PropertyInvalidatedCache.setTestMode(true);
+    }
 }
diff --git a/core/tests/coretests/src/android/os/IpcDataCacheTest.java b/core/tests/coretests/src/android/os/IpcDataCacheTest.java
index 64f77b3..5c56fdc 100644
--- a/core/tests/coretests/src/android/os/IpcDataCacheTest.java
+++ b/core/tests/coretests/src/android/os/IpcDataCacheTest.java
@@ -17,6 +17,7 @@
 package android.os;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 
 import android.multiuser.Flags;
 import android.platform.test.annotations.IgnoreUnderRavenwood;
@@ -26,6 +27,7 @@
 import androidx.test.filters.SmallTest;
 
 import org.junit.After;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 
@@ -92,14 +94,20 @@
         public Boolean apply(Integer x) {
             return mServer.query(x);
         }
+
         @Override
         public boolean shouldBypassCache(Integer x) {
             return x % 13 == 0;
         }
     }
 
-    // Clear the test mode after every test, in case this process is used for other
-    // tests. This also resets the test property map.
+    // Prepare for testing.
+    @Before
+    public void setUp() throws Exception {
+        IpcDataCache.setTestMode(true);
+    }
+
+    // Ensure all test configurations are cleared.
     @After
     public void tearDown() throws Exception {
         IpcDataCache.setTestMode(false);
@@ -119,9 +127,6 @@
                 new IpcDataCache<>(4, MODULE, API, "testCache1",
                         new ServerQuery(tester));
 
-        IpcDataCache.setTestMode(true);
-        testCache.testPropertyName();
-
         tester.verify(0);
         assertEquals(tester.value(3), testCache.query(3));
         tester.verify(1);
@@ -165,9 +170,6 @@
         IpcDataCache<Integer, Boolean> testCache =
                 new IpcDataCache<>(config, (x) -> tester.query(x, x % 10 == 9));
 
-        IpcDataCache.setTestMode(true);
-        testCache.testPropertyName();
-
         tester.verify(0);
         assertEquals(tester.value(3), testCache.query(3));
         tester.verify(1);
@@ -205,9 +207,6 @@
         IpcDataCache<Integer, Boolean> testCache =
                 new IpcDataCache<>(config, (x) -> tester.query(x), (x) -> x % 9 == 0);
 
-        IpcDataCache.setTestMode(true);
-        testCache.testPropertyName();
-
         tester.verify(0);
         assertEquals(tester.value(3), testCache.query(3));
         tester.verify(1);
@@ -313,8 +312,6 @@
         TestCache(String module, String api, TestQuery query) {
             super(4, module, api, "testCache7", query);
             mQuery = query;
-            setTestMode(true);
-            testPropertyName();
         }
 
         TestCache(IpcDataCache.Config c) {
@@ -324,8 +321,6 @@
         TestCache(IpcDataCache.Config c, TestQuery query) {
             super(c, query);
             mQuery = query;
-            setTestMode(true);
-            testPropertyName();
         }
 
         int getRecomputeCount() {
@@ -456,4 +451,52 @@
         TestCache ec = new TestCache(e);
         assertEquals(ec.isDisabled(), true);
     }
+
+    // Verify that test mode works properly.
+    @Test
+    public void testTestMode() {
+        // Create a cache that will write a system nonce.
+        TestCache sysCache = new TestCache(IpcDataCache.MODULE_SYSTEM, "mode1");
+        try {
+            // Invalidate the cache, which writes the system property.  There must be a permission
+            // failure.
+            sysCache.invalidateCache();
+            fail("expected permission failure");
+        } catch (RuntimeException e) {
+            // The expected exception is a bare RuntimeException.  The test does not attempt to
+            // validate the text of the exception message.
+        }
+
+        sysCache.testPropertyName();
+        // Invalidate the cache.  This must succeed because the property has been marked for
+        // testing.
+        sysCache.invalidateCache();
+
+        // Create a cache that uses MODULE_TEST.  Invalidation succeeds whether or not the
+        // property is tagged as being tested.
+        TestCache testCache = new TestCache(IpcDataCache.MODULE_TEST, "mode2");
+        testCache.invalidateCache();
+        testCache.testPropertyName();
+        testCache.invalidateCache();
+
+        // Clear test mode.  This fails if test mode is not enabled.
+        IpcDataCache.setTestMode(false);
+        try {
+            IpcDataCache.setTestMode(false);
+            fail("expected an IllegalStateException");
+        } catch (IllegalStateException e) {
+            // The expected exception.
+        }
+        // Configuring a property for testing must fail if test mode is false.
+        TestCache cache2 = new TestCache(IpcDataCache.MODULE_SYSTEM, "mode3");
+        try {
+            cache2.testPropertyName();
+            fail("expected an IllegalStateException");
+        } catch (IllegalStateException e) {
+            // The expected exception.
+        }
+
+        // Re-enable test mode (so that the cleanup for the test does not throw).
+        IpcDataCache.setTestMode(true);
+    }
 }
diff --git a/core/tests/coretests/src/android/view/LetterboxScrollProcessorTest.java b/core/tests/coretests/src/android/view/LetterboxScrollProcessorTest.java
index f8ec9f4..235625d 100644
--- a/core/tests/coretests/src/android/view/LetterboxScrollProcessorTest.java
+++ b/core/tests/coretests/src/android/view/LetterboxScrollProcessorTest.java
@@ -31,13 +31,13 @@
 import android.os.Looper;
 import android.platform.test.annotations.Presubmit;
 
+import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Before;
 import org.junit.Test;
 
-
 import java.util.ArrayList;
 import java.util.List;
 
@@ -54,30 +54,32 @@
     private LetterboxScrollProcessor mLetterboxScrollProcessor;
     private Context mContext;
 
-    // Constant delta used when comparing coordinates (floats)
+    // Constant delta used when comparing coordinates (floats).
     private static final float EPSILON = 0.1f;
 
+    private static final Rect APP_BOUNDS =
+            new Rect(/* left= */ 200, /* top= */ 200, /* right= */ 600, /* bottom= */ 1000);
+
     @Before
     public void setUp() {
         mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
 
         // Set app bounds as if it was letterboxed.
-        mContext.getResources().getConfiguration().windowConfiguration
-                .setBounds(new Rect(200, 200, 600, 1000));
-
-        Handler handler = new Handler(Looper.getMainLooper());
+        mContext.getResources().getConfiguration().windowConfiguration.setBounds(APP_BOUNDS);
 
         // Recreate to reset LetterboxScrollProcessor state.
-        mLetterboxScrollProcessor = new LetterboxScrollProcessor(mContext, handler);
+        mLetterboxScrollProcessor = new LetterboxScrollProcessor(mContext,
+                new Handler(Looper.getMainLooper()));
     }
 
     @Test
     public void testGestureInBoundsHasNoAdjustments() {
         // Tap-like gesture in bounds (non-scroll).
-        List<MotionEvent> tapGestureEvents = createTapGestureEvents(0f, 0f);
+        final List<MotionEvent> tapGestureEvents = createTapGestureEvents(
+                /* startX= */ 0f, /* startY= */ 0f);
 
         // Get processed events from Letterbox Scroll Processor.
-        List<MotionEvent> processedEvents = processMotionEvents(tapGestureEvents);
+        final List<MotionEvent> processedEvents = processMotionEvents(tapGestureEvents);
 
         // Ensure no changes are made to events after processing - event locations should not be
         // adjusted because the gesture started in the app's bounds (for all gestures).
@@ -87,13 +89,32 @@
     }
 
     @Test
-    public void testGestureOutsideBoundsIsIgnored() {
-        // Tap-like gesture outside bounds (non-scroll).
-        List<MotionEvent> tapGestureEvents = createTapGestureEvents(-100f, -100f);
+    public void testGestureInAppBoundsButOutsideTopWindowAlsoForwardedToTheApp() {
+        final Rect dialogBounds =
+                new Rect(/* left= */ 300, /* top= */ 500, /* right= */ 500, /* bottom= */ 700);
+        // Tap-like gesture outside the dialog, but in app bounds.
+        List<MotionEvent> tapGestureEvents = createTapGestureEventsWithCoordinateSystem(0f, 0f,
+                dialogBounds);
 
         // Get processed events from Letterbox Scroll Processor.
         List<MotionEvent> processedEvents = processMotionEvents(tapGestureEvents);
 
+        // Ensure no changes are made to events after processing - the event should be forwarded as
+        // normal.
+        assertEventLocationsAreNotAdjusted(tapGestureEvents, processedEvents);
+        // Ensure all of these events should be finished (expect no generated events).
+        assertMotionEventsShouldBeFinished(processedEvents);
+    }
+
+    @Test
+    public void testGestureOutsideBoundsIsIgnored() {
+        // Tap-like gesture outside bounds (non-scroll).
+        final List<MotionEvent> tapGestureEvents = createTapGestureEvents(
+                /* startX= */ -100f, /* startY= */ -100f);
+
+        // Get processed events from Letterbox Scroll Processor.
+        final List<MotionEvent> processedEvents = processMotionEvents(tapGestureEvents);
+
         // All events should be ignored since it was a non-scroll gesture and out of bounds.
         assertEquals(0, processedEvents.size());
     }
@@ -101,10 +122,11 @@
     @Test
     public void testScrollGestureInBoundsHasNoAdjustments() {
         // Scroll gesture in bounds (non-scroll).
-        List<MotionEvent> scrollGestureEvents = createScrollGestureEvents(0f, 0f);
+        final List<MotionEvent> scrollGestureEvents = createScrollGestureEvents(
+                /* startX= */ 0f, /* startY= */ 0f);
 
         // Get processed events from Letterbox Scroll Processor.
-        List<MotionEvent> processedEvents = processMotionEvents(scrollGestureEvents);
+        final List<MotionEvent> processedEvents = processMotionEvents(scrollGestureEvents);
 
         // Ensure no changes are made to events after processing - event locations should not be
         // adjusted because the gesture started in the app's bounds (for all gestures).
@@ -116,10 +138,11 @@
     @Test
     public void testScrollGestureInBoundsThenLeavesBoundsHasNoAdjustments() {
         // Scroll gesture in bounds (non-scroll) that moves out of bounds.
-        List<MotionEvent> scrollGestureEvents = createScrollGestureEvents(390f, 790f);
+        final List<MotionEvent> scrollGestureEvents = createScrollGestureEvents(
+                /* startX= */ 390f, /* startY= */ 790f);
 
         // Get processed events from Letterbox Scroll Processor.
-        List<MotionEvent> processedEvents = processMotionEvents(scrollGestureEvents);
+        final List<MotionEvent> processedEvents = processMotionEvents(scrollGestureEvents);
 
         // Ensure no changes are made to events after processing - event locations should not be
         // adjusted because the gesture started in the app's bounds (for all gestures), even if it
@@ -132,7 +155,8 @@
     @Test
     public void testScrollGestureOutsideBoundsIsStartedInBounds() {
         // Scroll gesture outside bounds.
-        List<MotionEvent> scrollGestureEvents = createScrollGestureEvents(-100f, 0f);
+        List<MotionEvent> scrollGestureEvents = createScrollGestureEvents(
+                /* startX= */ -100f, /* startY= */ 0f);
 
         // Get processed events from Letterbox Scroll Processor.
         List<MotionEvent> processedEvents = processMotionEvents(scrollGestureEvents);
@@ -142,9 +166,9 @@
 
         // Ensure offset ACTION_DOWN is first event received.
         MotionEvent firstProcessedEvent = processedEvents.getFirst();
-        assertEquals(firstProcessedEvent.getAction(), ACTION_DOWN);
-        assertEquals(firstProcessedEvent.getX(), 0, EPSILON);
-        assertEquals(firstProcessedEvent.getY(), 0, EPSILON);
+        assertEquals(ACTION_DOWN, firstProcessedEvent.getAction());
+        assertEquals(0, firstProcessedEvent.getX(), EPSILON);
+        assertEquals(0, firstProcessedEvent.getY(), EPSILON);
         // Ensure this event is not finished (because it was generated by LetterboxScrollProcessor).
         assertNull(mLetterboxScrollProcessor.processMotionEventBeforeFinish(firstProcessedEvent));
     }
@@ -152,16 +176,17 @@
     @Test
     public void testScrollGestureOutsideBoundsIsMovedInBounds() {
         // Scroll gesture outside bounds.
-        List<MotionEvent> scrollGestureEvents = createScrollGestureEvents(-100f, 0f);
+        final List<MotionEvent> scrollGestureEvents = createScrollGestureEvents(
+                /* startX= */ -100f, /* startY= */ 0f);
 
         // Get processed events from Letterbox Scroll Processor.
-        List<MotionEvent> processedEvents = processMotionEvents(scrollGestureEvents);
+        final List<MotionEvent> processedEvents = processMotionEvents(scrollGestureEvents);
 
         // When a scroll occurs outside bounds: once detected as a scroll, an offset ACTION_DOWN is
         // placed and then the rest of the gesture is offset also. Some ACTION_MOVE events may be
         // ignored until the gesture is 'detected as a scroll'.
         // For this test, we expect the first ACTION_MOVE event to be ignored:
-        scrollGestureEvents.remove(1);
+        scrollGestureEvents.remove(/* index= */ 1);
 
         // Ensure all processed events (that are not ignored) are offset over the app.
         assertXCoordinatesAdjustedToZero(scrollGestureEvents, processedEvents);
@@ -170,8 +195,9 @@
         assertMotionEventsShouldBeFinished(processedEvents.subList(1, processedEvents.size()));
     }
 
-    private List<MotionEvent> processMotionEvents(List<MotionEvent> motionEvents) {
-        List<MotionEvent> processedEvents = new ArrayList<>();
+    @NonNull
+    private List<MotionEvent> processMotionEvents(@NonNull List<MotionEvent> motionEvents) {
+        final List<MotionEvent> processedEvents = new ArrayList<>();
         for (MotionEvent motionEvent : motionEvents) {
             MotionEvent clonedEvent = MotionEvent.obtain(motionEvent);
             List<MotionEvent> letterboxScrollCompatEvents =
@@ -187,39 +213,76 @@
         return processedEvents;
     }
 
+    /**
+     * Creates and returns a tap gesture with X and Y in reference to the app bounds (top left
+     * corner is x=0, y=0).
+     */
+    @NonNull
     private List<MotionEvent> createTapGestureEvents(float startX, float startY) {
+        return createTapGestureEventsWithCoordinateSystem(startX, startY, APP_BOUNDS);
+    }
+
+    /**
+     * @param referenceWindowBounds the amount the event will be translated by.
+     */
+    private List<MotionEvent> createTapGestureEventsWithCoordinateSystem(float startX, float startY,
+            @NonNull Rect referenceWindowBounds) {
         // Events for tap-like gesture (non-scroll)
         List<MotionEvent> motionEvents = new ArrayList<>();
-        motionEvents.add(createBasicMotionEvent(0, ACTION_DOWN, startX, startY));
-        motionEvents.add(createBasicMotionEvent(10, ACTION_UP, startX , startY));
+        motionEvents.add(createBasicMotionEventWithCoordinateSystem(0, ACTION_DOWN,
+                startX, startY, referenceWindowBounds));
+        motionEvents.add(createBasicMotionEventWithCoordinateSystem(10, ACTION_UP,
+                startX , startY, referenceWindowBounds));
         return motionEvents;
     }
 
+    @NonNull
     private List<MotionEvent> createScrollGestureEvents(float startX, float startY) {
-        float touchSlop = (float) ViewConfiguration.get(mContext).getScaledTouchSlop();
+        final float touchSlop = (float) ViewConfiguration.get(mContext).getScaledTouchSlop();
 
-        // Events for scroll gesture (starts at (startX, startY) then moves down-right
-        List<MotionEvent> motionEvents = new ArrayList<>();
-        motionEvents.add(createBasicMotionEvent(0, ACTION_DOWN, startX, startY));
-        motionEvents.add(createBasicMotionEvent(10, ACTION_MOVE,
+        // Events for scroll gesture (starts at (startX, startY) then moves down-right.
+        final List<MotionEvent> motionEvents = new ArrayList<>();
+        motionEvents.add(createBasicMotionEvent(/* downTime= */ 0, ACTION_DOWN, startX, startY));
+        motionEvents.add(createBasicMotionEvent(/* downTime= */ 10, ACTION_MOVE,
                 startX + touchSlop / 2, startY + touchSlop / 2));
-        // Below event is first event in the scroll gesture where distance > touchSlop
-        motionEvents.add(createBasicMotionEvent(20, ACTION_MOVE,
+        // Below event is first event in the scroll gesture where distance > touchSlop.
+        motionEvents.add(createBasicMotionEvent(/* downTime= */ 20, ACTION_MOVE,
                 startX + touchSlop * 2, startY + touchSlop * 2));
-        motionEvents.add(createBasicMotionEvent(30, ACTION_MOVE,
+        motionEvents.add(createBasicMotionEvent(/* downTime= */ 30, ACTION_MOVE,
                 startX + touchSlop * 3, startY + touchSlop * 3));
-        motionEvents.add(createBasicMotionEvent(40, ACTION_UP,
+        motionEvents.add(createBasicMotionEvent(/* downTime= */ 40, ACTION_UP,
                 startX + touchSlop * 3, startY + touchSlop * 3));
         return motionEvents;
     }
 
-    private MotionEvent createBasicMotionEvent(int downTime, int action, float x, float y) {
-        return MotionEvent.obtain(0, downTime, action, x, y, 0);
+    /**
+     * Creates and returns an event with X and Y in reference to the app bounds (top left corner is
+     * x=0, y=0).
+     */
+    @NonNull
+    private MotionEvent createBasicMotionEvent(int eventTime, int action, float x, float y) {
+        return createBasicMotionEventWithCoordinateSystem(eventTime, action, x, y, APP_BOUNDS);
+    }
+
+    /**
+     * @param referenceWindowBounds the amount the event will be translated by.
+     */
+    @NonNull
+    private MotionEvent createBasicMotionEventWithCoordinateSystem(int eventTime, int action,
+            float x, float y, @NonNull Rect referenceWindowBounds) {
+        final float rawX = referenceWindowBounds.left + x;
+        final float rawY = referenceWindowBounds.top + y;
+        // RawX and RawY cannot be changed once the event is created. Therefore, pass rawX and rawY
+        // according to the app's bounds on the display, and then offset to make X and Y relative to
+        // the app's bounds.
+        final MotionEvent event = MotionEvent.obtain(0, eventTime, action, rawX, rawY, 0);
+        event.offsetLocation(-referenceWindowBounds.left, -referenceWindowBounds.top);
+        return event;
     }
 
     private void assertEventLocationsAreNotAdjusted(
-            List<MotionEvent> originalEvents,
-            List<MotionEvent> processedEvents) {
+            @NonNull List<MotionEvent> originalEvents,
+            @NonNull List<MotionEvent> processedEvents) {
         assertEquals("MotionEvent arrays are not the same size",
                 originalEvents.size(), processedEvents.size());
 
@@ -232,8 +295,8 @@
     }
 
     private void assertXCoordinatesAdjustedToZero(
-            List<MotionEvent> originalEvents,
-            List<MotionEvent> processedEvents) {
+            @NonNull List<MotionEvent> originalEvents,
+            @NonNull List<MotionEvent> processedEvents) {
         assertEquals("MotionEvent arrays are not the same size",
                 originalEvents.size(), processedEvents.size());
 
@@ -245,7 +308,7 @@
         }
     }
 
-    private void assertMotionEventsShouldBeFinished(List<MotionEvent> processedEvents) {
+    private void assertMotionEventsShouldBeFinished(@NonNull List<MotionEvent> processedEvents) {
         for (MotionEvent processedEvent : processedEvents) {
             assertNotNull(mLetterboxScrollProcessor.processMotionEventBeforeFinish(processedEvent));
         }
diff --git a/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java b/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java
index c3a5b19c94..1cbc7d6 100644
--- a/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java
+++ b/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java
@@ -40,7 +40,10 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.animation.AnimationHandler;
+import android.os.ConditionVariable;
 import android.os.Handler;
+import android.view.Choreographer;
 import android.view.FrameMetrics;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.JankData;
@@ -576,6 +579,44 @@
     }
 
     @Test
+    public void testEndAnimationWithLastFrameSyncId() {
+        final long[] expectedLastAnimationFrameVsyncId = { 0 };
+        final long[] lastAnimationFrameVsyncId = { 0 };
+        final long[] endAnimationVsyncId = { 0 };
+        final ConditionVariable condition = new ConditionVariable();
+        final FrameTracker tracker = spyFrameTracker(/* surfaceOnly= */ false);
+        mActivity.runOnUiThread(() -> {
+            final AnimationHandler animationHandler = AnimationHandler.getInstance();
+            final Choreographer realChoreographer = Choreographer.getInstance();
+            // 3 v-sync ids:
+            //  1. Begin mocked vsyncId = 0
+            //  2. Current real vsyncId = last animation frame
+            //  3. Posted real vsyncId = end animation
+            when(mChoreographer.getVsyncId()).thenReturn(0L);
+            tracker.begin();
+            mRunnableArgumentCaptor.getValue().run();
+            when(mChoreographer.getVsyncId()).thenAnswer(a -> realChoreographer.getVsyncId());
+            expectedLastAnimationFrameVsyncId[0] = realChoreographer.getVsyncId();
+            // Simulate ending multiple animators. Their callbacks should run in a batch.
+            animationHandler.postEndAnimationCallback(() -> {
+                endAnimationVsyncId[0] = realChoreographer.getVsyncId();
+                lastAnimationFrameVsyncId[0] =
+                        animationHandler.getLastAnimationFrameVsyncId(endAnimationVsyncId[0]);
+            });
+            animationHandler.postEndAnimationCallback(() -> {
+                tracker.end(FrameTracker.REASON_END_NORMAL);
+                condition.open();
+            });
+        });
+
+        condition.block(1000L /* timeoutMs */);
+        assertThat(lastAnimationFrameVsyncId[0]).isEqualTo(expectedLastAnimationFrameVsyncId[0]);
+        assertThat(endAnimationVsyncId[0]).isGreaterThan(lastAnimationFrameVsyncId[0]);
+        // Verifies that FrameTracker#mEndVsyncId uses the vsyncId from AnimationHandler.
+        verify(mJankStatsRegistration).removeAfter(eq(lastAnimationFrameVsyncId[0]));
+    }
+
+    @Test
     public void testMaxSuccessiveMissedFramesCount() {
         FrameTracker tracker = spyFrameTracker(/* surfaceOnly= */ true);
         when(mChoreographer.getVsyncId()).thenReturn(100L);
diff --git a/core/xsd/vts/Android.bp b/core/xsd/vts/Android.bp
index 5d8407f..239eed0 100644
--- a/core/xsd/vts/Android.bp
+++ b/core/xsd/vts/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_android_kernel",
     // See: http://go/android-license-faq
     // A large-scale-change added 'default_applicable_licenses' to import
     // all of the 'license_kinds' from "frameworks_base_license"
diff --git a/graphics/java/android/graphics/text/PositionedGlyphs.java b/graphics/java/android/graphics/text/PositionedGlyphs.java
index ed17fde..792e248 100644
--- a/graphics/java/android/graphics/text/PositionedGlyphs.java
+++ b/graphics/java/android/graphics/text/PositionedGlyphs.java
@@ -24,6 +24,7 @@
 import android.graphics.Paint;
 import android.graphics.Typeface;
 import android.graphics.fonts.Font;
+import android.os.Build;
 
 import com.android.internal.util.Preconditions;
 import com.android.text.flags.Flags;
@@ -53,6 +54,8 @@
                         Typeface.class.getClassLoader(), nReleaseFunc());
     }
 
+    private static boolean sIsRobolectric = Build.FINGERPRINT.equals("robolectric");
+
     private final long mLayoutPtr;
     private final float mXOffset;
     private final float mYOffset;
@@ -252,7 +255,7 @@
         mXOffset = xOffset;
         mYOffset = yOffset;
 
-        if (Flags.typefaceRedesign()) {
+        if (!sIsRobolectric && Flags.typefaceRedesign()) {
             int fontCount = nGetFontCount(layoutPtr);
             mFonts = new ArrayList<>(fontCount);
             for (int i = 0; i < fontCount; ++i) {
diff --git a/keystore/java/android/security/OWNERS b/keystore/java/android/security/OWNERS
index ed30587..32759b2 100644
--- a/keystore/java/android/security/OWNERS
+++ b/keystore/java/android/security/OWNERS
@@ -1 +1,2 @@
 per-file *.java,*.aidl = eranm@google.com,pgrafov@google.com,rubinxu@google.com
+per-file KeyStoreManager.java = mpgroover@google.com
diff --git a/keystore/java/android/security/keystore/KeyStoreManager.java b/keystore/java/android/security/keystore/KeyStoreManager.java
new file mode 100644
index 0000000..197aaba
--- /dev/null
+++ b/keystore/java/android/security/keystore/KeyStoreManager.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2024 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.security.keystore;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.security.KeyStore2;
+import android.security.KeyStoreException;
+import android.security.keystore2.AndroidKeyStoreProvider;
+import android.security.keystore2.AndroidKeyStorePublicKey;
+import android.system.keystore2.Domain;
+import android.system.keystore2.KeyDescriptor;
+import android.system.keystore2.KeyPermission;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.ByteArrayInputStream;
+import java.security.Key;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This class provides methods for interacting with keys stored within the
+ * <a href="/privacy-and-security/keystore">Android Keystore</a>.
+ */
+@FlaggedApi(android.security.Flags.FLAG_KEYSTORE_GRANT_API)
+@SystemService(Context.KEYSTORE_SERVICE)
+public class KeyStoreManager {
+    private static final String TAG = "KeyStoreManager";
+
+    private static final Object sInstanceLock = new Object();
+    @GuardedBy("sInstanceLock")
+    private static KeyStoreManager sInstance;
+
+    private final KeyStore2 mKeyStore2;
+
+    /**
+     * Private constructor to ensure only a single instance is created.
+     */
+    private KeyStoreManager() {
+        mKeyStore2 = KeyStore2.getInstance();
+    }
+
+    /**
+     * Returns the single instance of the {@code KeyStoreManager}.
+     *
+     * @hide
+     */
+    public static KeyStoreManager getInstance() {
+        synchronized (sInstanceLock) {
+            if (sInstance == null) {
+                sInstance = new KeyStoreManager();
+            }
+            return sInstance;
+        }
+    }
+
+    /**
+     * Grants access to the key owned by the calling app stored under the specified {@code alias}
+     * to another app on the device with the provided {@code uid}.
+     *
+     * <p>This method supports granting access to instances of both {@link javax.crypto.SecretKey}
+     * and {@link java.security.PrivateKey}. The resulting ID will persist across reboots and can be
+     * used by the grantee app for the life of the key or until access is revoked with {@link
+     * #revokeKeyAccess(String, int)}.
+     *
+     * <p>If the provided {@code alias} does not correspond to a key in the Android KeyStore, then
+     * an {@link UnrecoverableKeyException} is thrown.
+     *
+     * @param alias the alias of the key to be granted to another app
+     * @param uid   the uid of the app to which the key should be granted
+     * @return the ID of the granted key; this can be shared with the specified app, and that
+     * app can use {@link #getGrantedKeyFromId(long)} to access the key
+     * @throws UnrecoverableKeyException if the specified key cannot be recovered
+     * @throws KeyStoreException if an error is encountered when attempting to grant access to
+     * the key
+     * @see #getGrantedKeyFromId(long)
+     */
+    public long grantKeyAccess(@NonNull String alias, int uid)
+            throws KeyStoreException, UnrecoverableKeyException {
+        KeyDescriptor keyDescriptor = createKeyDescriptorFromAlias(alias);
+        final int grantAccessVector = KeyPermission.USE | KeyPermission.GET_INFO;
+        // When a key is in the GRANT domain, the nspace field of the KeyDescriptor contains its ID.
+        KeyDescriptor result = null;
+        try {
+            result = mKeyStore2.grant(keyDescriptor, uid, grantAccessVector);
+        } catch (KeyStoreException e) {
+            // If the provided alias does not correspond to a valid key in the KeyStore, then throw
+            // an UnrecoverableKeyException to remain consistent with other APIs in this class.
+            if (e.getNumericErrorCode() == KeyStoreException.ERROR_KEY_DOES_NOT_EXIST) {
+                throw new UnrecoverableKeyException("No key found by the given alias");
+            }
+            throw e;
+        }
+        if (result == null) {
+            Log.e(TAG, "Received a null KeyDescriptor from grant");
+            throw new KeyStoreException(KeyStoreException.ERROR_INTERNAL_SYSTEM_ERROR,
+                    "No ID was returned for the grant request for alias " + alias + " to uid "
+                            + uid);
+        } else if (result.domain != Domain.GRANT) {
+            Log.e(TAG, "Received a result outside the grant domain: " + result.domain);
+            throw new KeyStoreException(KeyStoreException.ERROR_INTERNAL_SYSTEM_ERROR,
+                    "Unable to obtain a grant ID for alias " + alias + " to uid " + uid);
+        }
+        return result.nspace;
+    }
+
+    /**
+     * Revokes access to the key in the app's namespace stored under the specified {@code
+     * alias} that was previously granted to another app on the device with the provided
+     * {@code uid}.
+     *
+     * <p>If the provided {@code alias} does not correspond to a key in the Android KeyStore, then
+     * an {@link UnrecoverableKeyException} is thrown.
+     *
+     * @param alias the alias of the key to be revoked from another app
+     * @param uid   the uid of the app from which the key access should be revoked
+     * @throws UnrecoverableKeyException if the specified key cannot be recovered
+     * @throws KeyStoreException if an error is encountered when attempting to revoke access
+     * to the key
+     */
+    public void revokeKeyAccess(@NonNull String alias, int uid)
+            throws KeyStoreException, UnrecoverableKeyException {
+        KeyDescriptor keyDescriptor = createKeyDescriptorFromAlias(alias);
+        try {
+            mKeyStore2.ungrant(keyDescriptor, uid);
+        } catch (KeyStoreException e) {
+            // If the provided alias does not correspond to a valid key in the KeyStore, then throw
+            // an UnrecoverableKeyException to remain consistent with other APIs in this class.
+            if (e.getNumericErrorCode() == KeyStoreException.ERROR_KEY_DOES_NOT_EXIST) {
+                throw new UnrecoverableKeyException("No key found by the given alias");
+            }
+            throw e;
+        }
+    }
+
+    /**
+     * Returns the key with the specified {@code id} that was previously shared with the
+     * app.
+     *
+     * <p>This method can return instances of both {@link javax.crypto.SecretKey} and {@link
+     * java.security.PrivateKey}. If a key with the provide {@code id} has not been granted to the
+     * caller, then an {@link UnrecoverableKeyException} is thrown.
+     *
+     * @param id the ID of the key that was shared with the app
+     * @return the {@link Key} that was shared with the app
+     * @throws UnrecoverableKeyException          if the specified key cannot be recovered
+     * @throws KeyPermanentlyInvalidatedException if the specified key was authorized to only
+     *                                            be used if the user has been authenticated and a
+     *                                            change has been made to the users
+     *                                            lockscreen or biometric enrollment that
+     *                                            permanently invalidates the key
+     * @see #grantKeyAccess(String, int)
+     */
+    public @NonNull Key getGrantedKeyFromId(long id)
+            throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
+        Key result = AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore(mKeyStore2, null,
+                id, Domain.GRANT);
+        if (result == null) {
+            throw new UnrecoverableKeyException("No key found by the given alias");
+        }
+        return result;
+    }
+
+    /**
+     * Returns a {@link KeyPair} containing the public and private key associated with
+     * the key that was previously shared with the app under the provided {@code id}.
+     *
+     * <p>If a {@link java.security.PrivateKey} has not been granted to the caller with the
+     * specified {@code id}, then an {@link UnrecoverableKeyException} is thrown.
+     *
+     * @param id the ID of the private key that was shared with the app
+     * @return a KeyPair containing the public and private key shared with the app
+     * @throws UnrecoverableKeyException          if the specified key cannot be recovered
+     * @throws KeyPermanentlyInvalidatedException if the specified key was authorized to only
+     *                                            be used if the user has been authenticated and a
+     *                                            change has been made to the users
+     *                                            lockscreen or biometric enrollment that
+     *                                            permanently invalidates the key
+     */
+    public @NonNull KeyPair getGrantedKeyPairFromId(long id)
+            throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
+        KeyDescriptor keyDescriptor = createKeyDescriptorFromId(id, Domain.GRANT);
+        return AndroidKeyStoreProvider.loadAndroidKeyStoreKeyPairFromKeystore(mKeyStore2,
+                keyDescriptor);
+    }
+
+    /**
+     * Returns a {@link List} of {@link X509Certificate} instances representing the certificate
+     * chain for the key that was previously shared with the app under the provided {@code id}.
+     *
+     * <p>If a {@link java.security.PrivateKey} has not been granted to the caller with the
+     * specified {@code id}, then an {@link UnrecoverableKeyException} is thrown.
+     *
+     * @param id the ID of the asymmetric key that was shared with the app
+     * @return a List of X509Certificates with the certificate at index 0 corresponding to
+     * the private key shared with the app
+     * @throws UnrecoverableKeyException          if the specified key cannot be recovered
+     * @throws KeyPermanentlyInvalidatedException if the specified key was authorized to only
+     *                                            be used if the user has been authenticated and a
+     *                                            change has been made to the users
+     *                                            lockscreen or biometric enrollment that
+     *                                            permanently invalidates the key
+     * @see #grantKeyAccess(String, int)
+     */
+    // Java APIs should prefer mutable collection return types with the exception being
+    // Collection.empty return types.
+    @SuppressWarnings("MixedMutabilityReturnType")
+    public @NonNull List<X509Certificate> getGrantedCertificateChainFromId(long id)
+            throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
+        KeyDescriptor keyDescriptor = createKeyDescriptorFromId(id, Domain.GRANT);
+        KeyPair keyPair = AndroidKeyStoreProvider.loadAndroidKeyStoreKeyPairFromKeystore(mKeyStore2,
+                keyDescriptor);
+        PublicKey keyStoreKey = keyPair.getPublic();
+        if (keyStoreKey instanceof AndroidKeyStorePublicKey) {
+            AndroidKeyStorePublicKey androidKeyStorePublicKey =
+                    (AndroidKeyStorePublicKey) keyStoreKey;
+            byte[] certBytes = androidKeyStorePublicKey.getCertificate();
+            X509Certificate cert = getCertificate(certBytes);
+            // If the leaf certificate is null, then a chain should not exist either
+            if (cert == null) {
+                return Collections.emptyList();
+            }
+            List<X509Certificate> result = new ArrayList<>();
+            result.add(cert);
+            byte[] certificateChain = androidKeyStorePublicKey.getCertificateChain();
+            Collection<X509Certificate> certificates = getCertificates(certificateChain);
+            result.addAll(certificates);
+            return result;
+        } else {
+            Log.e(TAG, "keyStoreKey is not of the expected type: " + keyStoreKey);
+        }
+        return Collections.emptyList();
+    }
+
+    /**
+     * Returns an {@link X509Certificate} instance from the provided {@code certificate} byte
+     * encoding of the certificate, or null if the provided encoding is null.
+     */
+    private static X509Certificate getCertificate(byte[] certificate) {
+        X509Certificate result = null;
+        if (certificate != null) {
+            try {
+                CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+                result = (X509Certificate) certificateFactory.generateCertificate(
+                        new ByteArrayInputStream(certificate));
+            } catch (Exception e) {
+                Log.e(TAG, "Caught an exception parsing the certificate: ", e);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Returns a {@link Collection} of {@link X509Certificate} instances from the provided
+     * {@code certificateChain} byte encoding of the certificates, or null if the provided
+     * encoding is null.
+     */
+    private static Collection<X509Certificate> getCertificates(byte[] certificateChain) {
+        if (certificateChain != null) {
+            try {
+                CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+                Collection<X509Certificate> certificates =
+                        (Collection<X509Certificate>) certificateFactory.generateCertificates(
+                                new ByteArrayInputStream(certificateChain));
+                if (certificates == null) {
+                    Log.e(TAG, "Received null certificates from a non-null certificateChain");
+                    return Collections.emptyList();
+                }
+                return certificates;
+            } catch (Exception e) {
+                Log.e(TAG, "Caught an exception parsing the certs: ", e);
+            }
+        }
+        return Collections.emptyList();
+    }
+
+    /**
+     * Returns a new {@link KeyDescriptor} instance in the app domain / namespace with the {@code
+     * alias} set to the provided value.
+     */
+    private static KeyDescriptor createKeyDescriptorFromAlias(String alias) {
+        KeyDescriptor keyDescriptor = new KeyDescriptor();
+        keyDescriptor.domain = Domain.APP;
+        keyDescriptor.nspace = KeyProperties.NAMESPACE_APPLICATION;
+        keyDescriptor.alias = alias;
+        keyDescriptor.blob = null;
+        return keyDescriptor;
+    }
+
+    /**
+     * Returns a new {@link KeyDescriptor} instance in the provided {@code domain} with the nspace
+     * field set to the provided {@code id}.
+     */
+    private static KeyDescriptor createKeyDescriptorFromId(long id, int domain) {
+        KeyDescriptor keyDescriptor = new KeyDescriptor();
+        keyDescriptor.domain = domain;
+        keyDescriptor.nspace = id;
+        keyDescriptor.alias = null;
+        keyDescriptor.blob = null;
+        return keyDescriptor;
+    }
+}
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java
index 99100de..dcc8844 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java
@@ -17,6 +17,7 @@
 package android.security.keystore2;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.security.KeyStore2;
 import android.security.KeyStoreSecurityLevel;
 import android.security.keymaster.KeymasterDefs;
@@ -335,11 +336,11 @@
     }
 
     /**
-     * Loads an an AndroidKeyStoreKey from the AndroidKeyStore backend.
+     * Loads an AndroidKeyStoreKey from the AndroidKeyStore backend.
      *
      * @param keyStore The keystore2 backend.
      * @param alias The alias of the key in the Keystore database.
-     * @param namespace The a Keystore namespace. This is used by system api only to request
+     * @param namespace The Keystore namespace. This is used by system api only to request
      *         Android system specific keystore namespace, which can be configured
      *         in the device's SEPolicy. Third party apps and most system components
      *         set this parameter to -1 to indicate their application specific namespace.
@@ -351,14 +352,40 @@
     public static AndroidKeyStoreKey loadAndroidKeyStoreKeyFromKeystore(
             @NonNull KeyStore2 keyStore, @NonNull String alias, int namespace)
             throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
-        KeyDescriptor descriptor = new KeyDescriptor();
+        int descriptorNamespace;
+        int descriptorDomain;
         if (namespace == KeyProperties.NAMESPACE_APPLICATION) {
-            descriptor.nspace = KeyProperties.NAMESPACE_APPLICATION; // ignored;
-            descriptor.domain = Domain.APP;
+            descriptorNamespace = KeyProperties.NAMESPACE_APPLICATION; // ignored;
+            descriptorDomain = Domain.APP;
         } else {
-            descriptor.nspace = namespace;
-            descriptor.domain = Domain.SELINUX;
+            descriptorNamespace = namespace;
+            descriptorDomain = Domain.SELINUX;
         }
+        return loadAndroidKeyStoreKeyFromKeystore(keyStore, alias, descriptorNamespace,
+                descriptorDomain);
+    }
+
+    /**
+     * Loads an AndroidKeyStoreKey from the AndroidKeyStore backend.
+     *
+     * @param keyStore The keystore2 backend
+     * @param alias The alias of the key in the Keystore database
+     * @param namespace The Keystore namespace. This is used by system api only to request
+     *         Android system specific keystore namespace, which can be configured
+     *         in the device's SEPolicy. Third party apps and most system components
+     *         set this parameter to -1 to indicate their application specific namespace.
+     *         See <a href="https://source.android.com/security/keystore#access-control">
+     *             Keystore 2.0 access control</a>
+     * @param domain The Keystore domain
+     * @return an AndroidKeyStoreKey corresponding to the provided values for the KeyDescriptor
+     * @hide
+     */
+    public static AndroidKeyStoreKey loadAndroidKeyStoreKeyFromKeystore(@NonNull KeyStore2 keyStore,
+            @Nullable String alias, long namespace, int domain)
+            throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
+        KeyDescriptor descriptor = new KeyDescriptor();
+        descriptor.nspace = namespace;
+        descriptor.domain = domain;
         descriptor.alias = alias;
         descriptor.blob = null;
 
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java b/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java
index 0b3be32..bcf619b 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java
@@ -44,6 +44,22 @@
         mEncoded = x509EncodedForm;
     }
 
+    /**
+     * Returns the byte array encoding of the certificate corresponding to this public key.
+     * @hide
+     */
+    public byte[] getCertificate() {
+        return mCertificate;
+    }
+
+    /**
+     * Returns the byte array encoding of the certificate chain for this public key.
+     * @hide
+     */
+    public byte[] getCertificateChain() {
+        return mCertificateChain;
+    }
+
     abstract AndroidKeyStorePrivateKey getPrivateKey();
 
     @Override
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index 7f11fea..2ab0310 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -57,9 +57,9 @@
      */
     private static final int NO_LEVEL_OVERRIDE = -1;
 
-    private static final int EXTENSIONS_VERSION_V7 = 7;
+    private static final int EXTENSIONS_VERSION_V8 = 8;
 
-    private static final int EXTENSIONS_VERSION_V6 = 6;
+    private static final int EXTENSIONS_VERSION_V7 = 7;
 
     private final Object mLock = new Object();
     private volatile DeviceStateManagerFoldingFeatureProducer mFoldingFeatureProducer;
@@ -80,12 +80,10 @@
      */
     @VisibleForTesting
     static int getExtensionsVersionCurrentPlatform() {
-        if (Flags.activityEmbeddingAnimationCustomizationFlag()) {
-            // Activity Embedding animation customization is the only major feature for v7.
-            return EXTENSIONS_VERSION_V7;
-        } else {
-            return EXTENSIONS_VERSION_V6;
+        if (Flags.aeBackStackRestore()) {
+            return EXTENSIONS_VERSION_V8;
         }
+        return EXTENSIONS_VERSION_V7;
     }
 
     private String generateLogMessage() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 19b51f1..e4db7b6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -164,6 +164,9 @@
      */
     private BackTouchTracker mQueuedTracker = new BackTouchTracker();
 
+    private final BackTransitionObserver mBackTransitionObserver =
+            new BackTransitionObserver();
+
     private final Runnable mAnimationTimeoutRunnable = () -> {
         ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation didn't finish in %d ms. Resetting...",
                 MAX_ANIMATION_DURATION);
@@ -268,6 +271,8 @@
         mBackTransitionHandler = new BackTransitionHandler();
         mTransitions.addHandler(mBackTransitionHandler);
         mHandler = handler;
+        mTransitions.registerObserver(mBackTransitionObserver);
+        mBackTransitionObserver.setBackTransitionHandler(mBackTransitionHandler);
         updateTouchableArea();
     }
 
@@ -729,6 +734,13 @@
     }
 
     /**
+     * @return Latest task id which back gesture has occurred on it.
+     */
+    public int getLatestTriggerBackTask() {
+        return mBackTransitionObserver.mFocusedTaskId;
+    }
+
+    /**
      * Sets to true when the back gesture has passed the triggering threshold, false otherwise.
      */
     public void setTriggerBack(boolean triggerBack) {
@@ -792,6 +804,11 @@
         boolean triggerBack = activeTouchTracker.getTriggerBack();
         ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", triggerBack);
 
+        if (triggerBack) {
+            mBackTransitionObserver.update(mBackNavigationInfo != null
+                            ? mBackNavigationInfo.getFocusedTaskId()
+                            : INVALID_TASK_ID);
+        }
         // Reset gesture states.
         mThresholdCrossed = false;
         mPointersPilfered = false;
@@ -1218,6 +1235,7 @@
             }
 
             if (shouldCancelAnimation(info)) {
+                mPrepareOpenTransition = null;
                 return false;
             }
 
@@ -1645,4 +1663,58 @@
     private static boolean canBeTransitionTarget(TransitionInfo.Change change) {
         return findComponentName(change) != null || findTaskId(change) != INVALID_TASK_ID;
     }
+
+    // Record the latest back gesture happen on which task.
+    static class BackTransitionObserver implements Transitions.TransitionObserver {
+        int mFocusedTaskId = INVALID_TASK_ID;
+        IBinder mFocusTaskMonitorToken;
+        private BackTransitionHandler mBackTransitionHandler;
+        void setBackTransitionHandler(BackTransitionHandler handler) {
+            mBackTransitionHandler = handler;
+        }
+
+        void update(int focusedTaskId) {
+            mFocusedTaskId = focusedTaskId;
+        }
+
+        @Override
+        public void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info,
+                @NonNull SurfaceControl.Transaction startTransaction,
+                @NonNull SurfaceControl.Transaction finishTransaction) {
+            if (mFocusedTaskId == INVALID_TASK_ID) {
+                return;
+            }
+            for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+                final TransitionInfo.Change c = info.getChanges().get(i);
+                if (c.getTaskInfo() != null && c.getTaskInfo().taskId == mFocusedTaskId) {
+                    mFocusTaskMonitorToken = transition;
+                    break;
+                }
+            }
+            // Transition happen but the task isn't involved, reset.
+            if (mFocusTaskMonitorToken == null) {
+                mFocusedTaskId = INVALID_TASK_ID;
+            }
+        }
+
+        @Override
+        public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {
+            if (mFocusTaskMonitorToken == merged) {
+                mFocusTaskMonitorToken = playing;
+            }
+            if (mBackTransitionHandler.mClosePrepareTransition == merged) {
+                mBackTransitionHandler.mClosePrepareTransition = null;
+            }
+        }
+
+        @Override
+        public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {
+            if (mFocusTaskMonitorToken == transition) {
+                mFocusedTaskId = INVALID_TASK_ID;
+            }
+            if (mBackTransitionHandler.mClosePrepareTransition == transition) {
+                mBackTransitionHandler.mClosePrepareTransition = null;
+            }
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt
index 13a805a..e71b4f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt
@@ -95,15 +95,15 @@
     override fun addToContainer(menuView: ManageWindowsView) {
         val menuPosition = calculateMenuPosition()
         menuViewContainer = AdditionalSystemViewContainer(
-            windowManagerWrapper,
-            callerTaskInfo.taskId,
-            menuPosition.x,
-            menuPosition.y,
-            menuView.menuWidth,
-            menuView.menuHeight,
-            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
+            windowManagerWrapper = windowManagerWrapper,
+            taskId = callerTaskInfo.taskId,
+            x = menuPosition.x,
+            y = menuPosition.y,
+            width = menuView.menuWidth,
+            height = menuView.menuHeight,
+            flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
                     WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
-            menuView.rootView
+            view = menuView.rootView,
         )
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt
index 05391a8..173bc08 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt
@@ -22,14 +22,19 @@
 import android.graphics.Point
 import android.view.SurfaceControl
 import android.view.SurfaceControlViewHost
+import android.view.WindowInsets.Type.systemBars
 import android.view.WindowManager
 import android.view.WindowlessWindowManager
 import android.window.TaskConstants
 import android.window.TaskSnapshot
 import androidx.compose.ui.graphics.toArgb
+import com.android.internal.annotations.VisibleForTesting
+import com.android.window.flags.Flags
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.desktopmode.DesktopRepository
 import com.android.wm.shell.shared.desktopmode.ManageWindowsViewContainer
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer
 import com.android.wm.shell.windowdecor.common.DecorThemeUtil
@@ -41,9 +46,12 @@
  */
 class DesktopHeaderManageWindowsMenu(
     private val callerTaskInfo: RunningTaskInfo,
+    private val x: Int,
+    private val y: Int,
     private val displayController: DisplayController,
     private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer,
     context: Context,
+    private val desktopRepository: DesktopRepository,
     private val surfaceControlBuilderSupplier: Supplier<SurfaceControl.Builder>,
     private val surfaceControlTransactionSupplier: Supplier<SurfaceControl.Transaction>,
     snapshotList: List<Pair<Int, TaskSnapshot>>,
@@ -53,7 +61,8 @@
     context,
     DecorThemeUtil(context).getColorScheme(callerTaskInfo).background.toArgb()
 ) {
-    private var menuViewContainer: AdditionalViewContainer? = null
+    @VisibleForTesting
+    var menuViewContainer: AdditionalViewContainer? = null
 
     init {
         show(snapshotList, onIconClickListener, onOutsideClickListener)
@@ -64,8 +73,37 @@
     }
 
     override fun addToContainer(menuView: ManageWindowsView) {
-        val taskBounds = callerTaskInfo.getConfiguration().windowConfiguration.bounds
-        val menuPosition = Point(taskBounds.left, taskBounds.top)
+        val menuPosition = Point(x, y)
+        val flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
+                WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or
+                WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+        menuViewContainer = if (Flags.enableFullyImmersiveInDesktop()
+            && desktopRepository.isTaskInFullImmersiveState(callerTaskInfo.taskId)) {
+            // Use system view container so that forcibly shown system bars take effect in
+            // immersive.
+            createAsSystemViewContainer(menuPosition, flags)
+        } else {
+            createAsViewHostContainer(menuPosition, flags)
+        }
+    }
+
+    private fun createAsSystemViewContainer(position: Point, flags: Int): AdditionalViewContainer {
+        return AdditionalSystemViewContainer(
+            windowManagerWrapper = WindowManagerWrapper(
+                context.getSystemService(WindowManager::class.java)
+            ),
+            taskId = callerTaskInfo.taskId,
+            x = position.x,
+            y = position.y,
+            width = menuView.menuWidth,
+            height = menuView.menuHeight,
+            flags = flags,
+            forciblyShownTypes = systemBars(),
+            view = menuView.rootView
+        )
+    }
+
+    private fun createAsViewHostContainer(position: Point, flags: Int): AdditionalViewContainer {
         val builder = surfaceControlBuilderSupplier.get()
         rootTdaOrganizer.attachToDisplayArea(callerTaskInfo.displayId, builder)
         val leash = builder
@@ -73,11 +111,10 @@
             .setContainerLayer()
             .build()
         val lp = WindowManager.LayoutParams(
-            menuView.menuWidth, menuView.menuHeight,
+            menuView.menuWidth,
+            menuView.menuHeight,
             WindowManager.LayoutParams.TYPE_APPLICATION,
-            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-                    or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
-                    or WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
+            flags,
             PixelFormat.TRANSPARENT
         )
         val windowManager = WindowlessWindowManager(
@@ -93,11 +130,12 @@
         menuView.let { viewHost.setView(it.rootView, lp) }
         val t = surfaceControlTransactionSupplier.get()
         t.setLayer(leash, TaskConstants.TASK_CHILD_LAYER_FLOATING_MENU)
-            .setPosition(leash, menuPosition.x.toFloat(), menuPosition.y.toFloat())
+            .setPosition(leash, position.x.toFloat(), position.y.toFloat())
             .show(leash)
         t.apply()
-        menuViewContainer = AdditionalViewHostViewContainer(
-            leash, viewHost,
+        return AdditionalViewHostViewContainer(
+            leash,
+            viewHost,
             surfaceControlTransactionSupplier
         )
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index d3b7ca1..6eb20b9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -454,7 +454,13 @@
         }
 
         if (isHandleMenuActive()) {
-            mHandleMenu.relayout(startT, mResult.mCaptionX);
+            mHandleMenu.relayout(
+                    startT,
+                    mResult.mCaptionX,
+                    // Add top padding to the caption Y so that the menu is shown over what is the
+                    // actual contents of the caption, ignoring padding. This is currently relevant
+                    // to the Header in desktop immersive.
+                    mResult.mCaptionY + mResult.mCaptionTopPadding);
         }
 
         if (isOpenByDefaultDialogActive()) {
@@ -1258,6 +1264,8 @@
                 && Flags.enableDesktopWindowingMultiInstanceFeatures();
         final boolean shouldShowManageWindowsButton = supportsMultiInstance
                 && mMinimumInstancesFound;
+        final boolean inDesktopImmersive = mDesktopRepository
+                .isTaskInFullImmersiveState(mTaskInfo.taskId);
         mHandleMenu = mHandleMenuFactory.create(
                 this,
                 mWindowManagerWrapper,
@@ -1271,7 +1279,11 @@
                 getBrowserLink(),
                 mResult.mCaptionWidth,
                 mResult.mCaptionHeight,
-                mResult.mCaptionX
+                mResult.mCaptionX,
+                // Add top padding to the caption Y so that the menu is shown over what is the
+                // actual contents of the caption, ignoring padding. This is currently relevant
+                // to the Header in desktop immersive.
+                mResult.mCaptionY + mResult.mCaptionTopPadding
         );
         mWindowDecorViewHolder.onHandleMenuOpened();
         mHandleMenu.show(
@@ -1302,7 +1314,8 @@
                 /* onOutsideTouchListener= */ () -> {
                     closeHandleMenu();
                     return Unit.INSTANCE;
-                }
+                },
+                /* forceShowSystemBars= */ inDesktopImmersive
         );
         if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
             notifyCaptionStateChanged();
@@ -1316,9 +1329,12 @@
         if (mTaskInfo.isFreeform()) {
             mManageWindowsMenu = new DesktopHeaderManageWindowsMenu(
                     mTaskInfo,
+                    /* x= */ mResult.mCaptionX,
+                    /* y= */ mResult.mCaptionY + mResult.mCaptionTopPadding,
                     mDisplayController,
                     mRootTaskDisplayAreaOrganizer,
                     mContext,
+                    mDesktopRepository,
                     mSurfaceControlBuilderSupplier,
                     mSurfaceControlTransactionSupplier,
                     snapshotList,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index 2e32703..93bd929 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -32,6 +32,7 @@
 import android.view.MotionEvent.ACTION_OUTSIDE
 import android.view.SurfaceControl
 import android.view.View
+import android.view.WindowInsets.Type.systemBars
 import android.view.WindowManager
 import android.widget.Button
 import android.widget.ImageButton
@@ -73,7 +74,8 @@
     private val openInBrowserIntent: Intent?,
     private val captionWidth: Int,
     private val captionHeight: Int,
-    captionX: Int
+    captionX: Int,
+    captionY: Int
 ) {
     private val context: Context = parentDecor.mDecorWindowContext
     private val taskInfo: RunningTaskInfo = parentDecor.mTaskInfo
@@ -110,7 +112,7 @@
         get() = openInBrowserIntent != null
 
     init {
-        updateHandleMenuPillPositions(captionX)
+        updateHandleMenuPillPositions(captionX, captionY)
     }
 
     fun show(
@@ -123,6 +125,7 @@
         onOpenByDefaultClickListener: () -> Unit,
         onCloseMenuClickListener: () -> Unit,
         onOutsideTouchListener: () -> Unit,
+        forceShowSystemBars: Boolean = false,
     ) {
         val ssg = SurfaceSyncGroup(TAG)
         val t = SurfaceControl.Transaction()
@@ -139,6 +142,7 @@
             onOpenByDefaultClickListener = onOpenByDefaultClickListener,
             onCloseMenuClickListener = onCloseMenuClickListener,
             onOutsideTouchListener = onOutsideTouchListener,
+            forceShowSystemBars = forceShowSystemBars,
         )
         ssg.addTransaction(t)
         ssg.markSyncReady()
@@ -157,7 +161,8 @@
         openInBrowserClickListener: (Intent) -> Unit,
         onOpenByDefaultClickListener: () -> Unit,
         onCloseMenuClickListener: () -> Unit,
-        onOutsideTouchListener: () -> Unit
+        onOutsideTouchListener: () -> Unit,
+        forceShowSystemBars: Boolean = false,
     ) {
         val handleMenuView = HandleMenuView(
             context = context,
@@ -185,7 +190,7 @@
         val x = handleMenuPosition.x.toInt()
         val y = handleMenuPosition.y.toInt()
         handleMenuViewContainer =
-            if (!taskInfo.isFreeform && Flags.enableHandleInputFix()) {
+            if ((!taskInfo.isFreeform && Flags.enableHandleInputFix()) || forceShowSystemBars) {
                 AdditionalSystemViewContainer(
                     windowManagerWrapper = windowManagerWrapper,
                     taskId = taskInfo.taskId,
@@ -196,7 +201,8 @@
                     flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
                             WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or
                             WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
-                    view = handleMenuView.rootView
+                    view = handleMenuView.rootView,
+                    forciblyShownTypes = if (forceShowSystemBars) { systemBars() } else { 0 }
                 )
             } else {
                 parentDecor.addWindow(
@@ -210,15 +216,15 @@
     /**
      * Updates handle menu's position variables to reflect its next position.
      */
-    private fun updateHandleMenuPillPositions(captionX: Int) {
+    private fun updateHandleMenuPillPositions(captionX: Int, captionY: Int) {
         val menuX: Int
         val menuY: Int
         val taskBounds = taskInfo.getConfiguration().windowConfiguration.bounds
-        updateGlobalMenuPosition(taskBounds, captionX)
+        updateGlobalMenuPosition(taskBounds, captionX, captionY)
         if (layoutResId == R.layout.desktop_mode_app_header) {
             // Align the handle menu to the left side of the caption.
             menuX = marginMenuStart
-            menuY = marginMenuTop
+            menuY = captionY + marginMenuTop
         } else {
             if (Flags.enableHandleInputFix()) {
                 // In a focused decor, we use global coordinates for handle menu. Therefore we
@@ -228,26 +234,26 @@
                 menuY = globalMenuPosition.y
             } else {
                 menuX = (taskBounds.width() / 2) - (menuWidth / 2)
-                menuY = marginMenuTop
+                menuY = captionY + marginMenuTop
             }
         }
         // Handle Menu position setup.
         handleMenuPosition.set(menuX.toFloat(), menuY.toFloat())
     }
 
-    private fun updateGlobalMenuPosition(taskBounds: Rect, captionX: Int) {
+    private fun updateGlobalMenuPosition(taskBounds: Rect, captionX: Int, captionY: Int) {
         val nonFreeformX = captionX + (captionWidth / 2) - (menuWidth / 2)
         when {
             taskInfo.isFreeform -> {
                 globalMenuPosition.set(
                     /* x = */ taskBounds.left + marginMenuStart,
-                    /* y = */ taskBounds.top + marginMenuTop
+                    /* y = */ taskBounds.top + captionY + marginMenuTop
                 )
             }
             taskInfo.isFullscreen -> {
                 globalMenuPosition.set(
                     /* x = */ nonFreeformX,
-                    /* y = */ marginMenuTop
+                    /* y = */ marginMenuTop + captionY
                 )
             }
             taskInfo.isMultiWindow -> {
@@ -261,13 +267,13 @@
                     SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT -> {
                         globalMenuPosition.set(
                             /* x = */ leftOrTopStageBounds.width() + nonFreeformX,
-                            /* y = */ marginMenuTop
+                            /* y = */ captionY + marginMenuTop
                         )
                     }
                     SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT -> {
                         globalMenuPosition.set(
                             /* x = */ nonFreeformX,
-                            /* y = */ marginMenuTop
+                            /* y = */ captionY + marginMenuTop
                         )
                     }
                 }
@@ -280,10 +286,11 @@
      */
     fun relayout(
         t: SurfaceControl.Transaction,
-        captionX: Int
+        captionX: Int,
+        captionY: Int,
     ) {
         handleMenuViewContainer?.let { container ->
-            updateHandleMenuPillPositions(captionX)
+            updateHandleMenuPillPositions(captionX, captionY)
             container.setPosition(t, handleMenuPosition.x, handleMenuPosition.y)
         }
     }
@@ -675,7 +682,8 @@
         openInBrowserIntent: Intent?,
         captionWidth: Int,
         captionHeight: Int,
-        captionX: Int
+        captionX: Int,
+        captionY: Int,
     ): HandleMenu
 }
 
@@ -694,7 +702,8 @@
         openInBrowserIntent: Intent?,
         captionWidth: Int,
         captionHeight: Int,
-        captionX: Int
+        captionX: Int,
+        captionY: Int,
     ): HandleMenu {
         return HandleMenu(
             parentDecor,
@@ -709,7 +718,8 @@
             openInBrowserIntent,
             captionWidth,
             captionHeight,
-            captionX
+            captionX,
+            captionY,
         )
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index ce5cfd0..6b3b357 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -256,6 +256,8 @@
         outResult.mCaptionWidth = params.mCaptionWidthId != Resources.ID_NULL
                 ? loadDimensionPixelSize(resources, params.mCaptionWidthId) : taskBounds.width();
         outResult.mCaptionX = (outResult.mWidth - outResult.mCaptionWidth) / 2;
+        outResult.mCaptionY = 0;
+        outResult.mCaptionTopPadding = params.mCaptionTopPadding;
 
         updateDecorationContainerSurface(startT, outResult);
         updateCaptionContainerSurface(startT, outResult);
@@ -786,6 +788,8 @@
         int mCaptionHeight;
         int mCaptionWidth;
         int mCaptionX;
+        int mCaptionY;
+        int mCaptionTopPadding;
         final Region mCustomizableCaptionRegion = Region.obtain();
         int mWidth;
         int mHeight;
@@ -797,6 +801,8 @@
             mCaptionHeight = 0;
             mCaptionWidth = 0;
             mCaptionX = 0;
+            mCaptionY = 0;
+            mCaptionTopPadding = 0;
             mCustomizableCaptionRegion.setEmpty();
             mRootView = null;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
index 1be26f0..8b6aaaf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
@@ -23,6 +23,7 @@
 import android.view.LayoutInflater
 import android.view.SurfaceControl
 import android.view.View
+import android.view.WindowInsets
 import android.view.WindowManager
 import com.android.wm.shell.windowdecor.WindowManagerWrapper
 
@@ -38,6 +39,7 @@
     width: Int,
     height: Int,
     flags: Int,
+    @WindowInsets.Type.InsetsType forciblyShownTypes: Int = 0,
     override val view: View
 ) : AdditionalViewContainer() {
     val lp: WindowManager.LayoutParams = WindowManager.LayoutParams(
@@ -49,6 +51,7 @@
         title = "Additional view container of Task=$taskId"
         gravity = Gravity.LEFT or Gravity.TOP
         setTrustedOverlay()
+        this.forciblyShownTypes = forciblyShownTypes
     }
 
     constructor(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt
new file mode 100644
index 0000000..f9f760e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.SurfaceControl
+import androidx.test.filters.SmallTest
+import com.android.window.flags.Flags
+import com.android.wm.shell.MockToken
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+/**
+ * Tests for [DesktopHeaderManageWindowsMenu].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:DesktopHeaderManageWindowsMenuTest
+ */
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class DesktopHeaderManageWindowsMenuTest : ShellTestCase() {
+
+    @JvmField
+    @Rule
+    val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
+    private lateinit var desktopRepository: DesktopRepository
+    private lateinit var menu: DesktopHeaderManageWindowsMenu
+
+    @Before
+    fun setUp() {
+        desktopRepository = DesktopRepository(
+            context = context,
+            shellInit = ShellInit(TestShellExecutor()),
+            persistentRepository = mock(),
+            mainCoroutineScope = mock()
+        )
+    }
+
+    @After
+    fun tearDown() {
+        menu.close()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun testShow_forImmersiveTask_usesSystemViewContainer() {
+        val task = createFreeformTask()
+        desktopRepository.setTaskInFullImmersiveState(
+            displayId = task.displayId,
+            taskId = task.taskId,
+            immersive = true
+        )
+
+        menu = createMenu(task)
+
+        assertThat(menu.menuViewContainer).isInstanceOf(AdditionalSystemViewContainer::class.java)
+    }
+
+    private fun createMenu(task: RunningTaskInfo) = DesktopHeaderManageWindowsMenu(
+        callerTaskInfo = task,
+        x = 0,
+        y = 0,
+        displayController = mock(),
+        rootTdaOrganizer = mock(),
+        context = context,
+        desktopRepository = desktopRepository,
+        surfaceControlBuilderSupplier = { SurfaceControl.Builder() },
+        surfaceControlTransactionSupplier = { SurfaceControl.Transaction() },
+        snapshotList = emptyList(),
+        onIconClickListener = {},
+        onOutsideClickListener = {},
+    )
+
+    private fun createFreeformTask(): RunningTaskInfo = TestRunningTaskInfoBuilder()
+        .setToken(MockToken().token())
+        .setActivityType(ACTIVITY_TYPE_STANDARD)
+        .setWindowingMode(WINDOWING_MODE_FREEFORM)
+        .build()
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 1d11d2e..3208872 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -259,7 +259,8 @@
         doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY);
         doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt());
         when(mMockHandleMenuFactory.create(any(), any(), anyInt(), any(), any(), any(),
-                anyBoolean(), anyBoolean(), anyBoolean(), any(), anyInt(), anyInt(), anyInt()))
+                anyBoolean(), anyBoolean(), anyBoolean(), any(), anyInt(), anyInt(), anyInt(),
+                anyInt()))
                 .thenReturn(mMockHandleMenu);
         when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any())).thenReturn(false);
         when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any(), any(),
@@ -1070,7 +1071,8 @@
                 openInBrowserCaptor.capture(),
                 any(),
                 any(),
-                any()
+                any(),
+                anyBoolean()
         );
         openInBrowserCaptor.getValue().invoke(new Intent(Intent.ACTION_MAIN, TEST_URI1));
 
@@ -1099,7 +1101,8 @@
                 openInBrowserCaptor.capture(),
                 any(),
                 any(),
-                any()
+                any(),
+                anyBoolean()
         );
 
         openInBrowserCaptor.getValue().invoke(new Intent(Intent.ACTION_MAIN, TEST_URI1));
@@ -1151,7 +1154,8 @@
                 any(),
                 any(),
                 closeClickListener.capture(),
-                any()
+                any(),
+                anyBoolean()
         );
 
         closeClickListener.getValue().invoke();
@@ -1161,6 +1165,30 @@
     }
 
     @Test
+    public void createHandleMenu_immersiveWindow_forceShowsSystemBars() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+        final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo,
+                true /* relayout */);
+        when(mMockDesktopRepository.isTaskInFullImmersiveState(taskInfo.taskId))
+                .thenReturn(true);
+
+        createHandleMenu(decoration);
+
+        verify(mMockHandleMenu).show(
+                any(),
+                any(),
+                any(),
+                any(),
+                any(),
+                any(),
+                any(),
+                any(),
+                any(),
+                /* forceShowSystemBars= */ eq(true)
+        );
+    }
+
+    @Test
     @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
     public void notifyCaptionStateChanged_flagDisabled_doNoNotify() {
         when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
@@ -1301,7 +1329,7 @@
         verify(mMockHandleMenuFactory).create(any(), any(), anyInt(), any(), any(),
                 any(), anyBoolean(), anyBoolean(), anyBoolean(),
                 argThat(intent -> (uri == null && intent == null) || intent.getData().equals(uri)),
-                anyInt(), anyInt(), anyInt());
+                anyInt(), anyInt(), anyInt(), anyInt());
     }
 
     private void createMaximizeMenu(DesktopModeWindowDecoration decoration, MaximizeMenu menu) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
index 1820133..9544fa8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
@@ -33,6 +33,7 @@
 import android.view.SurfaceControl
 import android.view.SurfaceControlViewHost
 import android.view.View
+import android.view.WindowInsets.Type.systemBars
 import android.view.WindowManager
 import androidx.core.graphics.toPointF
 import androidx.test.filters.SmallTest
@@ -186,13 +187,35 @@
         assertEquals(expected.toPointF(), handleMenu.handleMenuPosition)
     }
 
-    private fun createTaskInfo(windowingMode: Int, splitPosition: Int) {
+    @Test
+    fun testCreate_forceShowSystemBars_usesSystemViewContainer() {
+        createTaskInfo(WINDOWING_MODE_FREEFORM)
+
+        handleMenu = createAndShowHandleMenu(forceShowSystemBars = true)
+
+        // Only AdditionalSystemViewContainer supports force showing system bars.
+        assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer)
+    }
+
+    @Test
+    fun testCreate_forceShowSystemBars() {
+        createTaskInfo(WINDOWING_MODE_FREEFORM)
+
+        handleMenu = createAndShowHandleMenu(forceShowSystemBars = true)
+
+        val types = (handleMenu.handleMenuViewContainer as AdditionalSystemViewContainer)
+            .lp.forciblyShownTypes
+        assertTrue((types and systemBars()) != 0)
+    }
+
+    private fun createTaskInfo(windowingMode: Int, splitPosition: Int? = null) {
         val taskDescriptionBuilder = ActivityManager.TaskDescription.Builder()
             .setBackgroundColor(Color.YELLOW)
         val bounds = when (windowingMode) {
             WINDOWING_MODE_FULLSCREEN -> DISPLAY_BOUNDS
             WINDOWING_MODE_FREEFORM -> FREEFORM_BOUNDS
             WINDOWING_MODE_MULTI_WINDOW -> {
+                checkNotNull(splitPosition)
                 if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
                     SPLIT_LEFT_BOUNDS
                 } else {
@@ -208,14 +231,19 @@
             .setBounds(bounds)
             .setVisible(true)
             .build()
-        whenever(splitScreenController.getSplitPosition(any())).thenReturn(splitPosition)
-        whenever(splitScreenController.getStageBounds(any(), any())).thenAnswer {
-            (it.arguments.first() as Rect).set(SPLIT_LEFT_BOUNDS)
-            (it.arguments[1] as Rect).set(SPLIT_RIGHT_BOUNDS)
+        if (windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
+            whenever(splitScreenController.getSplitPosition(any())).thenReturn(splitPosition)
+            whenever(splitScreenController.getStageBounds(any(), any())).thenAnswer {
+                (it.arguments.first() as Rect).set(SPLIT_LEFT_BOUNDS)
+                (it.arguments[1] as Rect).set(SPLIT_RIGHT_BOUNDS)
+            }
         }
     }
 
-    private fun createAndShowHandleMenu(splitPosition: Int): HandleMenu {
+    private fun createAndShowHandleMenu(
+        splitPosition: Int? = null,
+        forceShowSystemBars: Boolean = false,
+    ): HandleMenu {
         val layoutId = if (mockDesktopWindowDecoration.mTaskInfo.isFreeform) {
             R.layout.desktop_mode_app_header
         } else {
@@ -225,6 +253,7 @@
             WINDOWING_MODE_FULLSCREEN -> (DISPLAY_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2)
             WINDOWING_MODE_FREEFORM -> 0
             WINDOWING_MODE_MULTI_WINDOW -> {
+                checkNotNull(splitPosition)
                 if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
                     (SPLIT_LEFT_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2)
                 } else {
@@ -238,9 +267,21 @@
             layoutId, appIcon, appName, splitScreenController, shouldShowWindowingPill = true,
             shouldShowNewWindowButton = true, shouldShowManageWindowsButton = false,
             null /* openInBrowserLink */, captionWidth = HANDLE_WIDTH, captionHeight = 50,
-            captionX = captionX
+            captionX = captionX,
+            captionY = 0,
         )
-        handleMenu.show(mock(), mock(), mock(), mock(), mock(), mock(), mock(), mock(), mock())
+        handleMenu.show(
+            onToDesktopClickListener = mock(),
+            onToFullscreenClickListener = mock(),
+            onToSplitScreenClickListener = mock(),
+            onNewWindowClickListener = mock(),
+            onManageWindowsClickListener = mock(),
+            openInBrowserClickListener = mock(),
+            onOpenByDefaultClickListener = mock(),
+            onCloseMenuClickListener = mock(),
+            onOutsideTouchListener = mock(),
+            forceShowSystemBars = forceShowSystemBars
+        )
         return handleMenu
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index bb41e9c..fb17ae9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -515,6 +515,23 @@
     }
 
     @Test
+    public void testRelayout_withPadding_setsOnResult() {
+        final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+                .setDisplayId(Display.DEFAULT_DISPLAY)
+                .setBounds(TASK_BOUNDS)
+                .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
+                .setVisible(true)
+                .build();
+        final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+        mRelayoutParams.mCaptionTopPadding = 50;
+
+        windowDecor.relayout(taskInfo, false /* applyStartTransactionOnDraw */,
+                true /* hasGlobalFocus */);
+
+        assertEquals(50, mRelayoutResult.mCaptionTopPadding);
+    }
+
+    @Test
     public void testRelayout_fluidResizeEnabled_freeformTask_setTaskSurfaceColor() {
         StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
                 DesktopModeStatus.class).strictness(
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 02ca307..c22b674 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -471,6 +471,7 @@
 
     List<AudioDeviceAttributes> getDevicesForAttributesUnprotected(in AudioAttributes attributes);
 
+    @EnforcePermission(anyOf = {"MODIFY_AUDIO_ROUTING", "QUERY_AUDIO_STATE"})
     void addOnDevicesForAttributesChangedListener(in AudioAttributes attributes,
             in IDevicesForAttributesCallback callback);
 
diff --git a/media/java/android/media/flags/projection.aconfig b/media/java/android/media/flags/projection.aconfig
index 9a9a073..17d1ff6 100644
--- a/media/java/android/media/flags/projection.aconfig
+++ b/media/java/android/media/flags/projection.aconfig
@@ -10,3 +10,11 @@
     bug: "323008518"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "media_projection_connected_display"
+    namespace: "virtual_devices"
+    description: "Enable recording connected display"
+    bug: "362720120"
+    is_exported: true
+}
diff --git a/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt
index 06214eb..8ef4c58 100644
--- a/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt
+++ b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt
@@ -99,15 +99,18 @@
         @Suppress("UNCHECKED_CAST")
         val clazz = preferenceScreenCreator.fragmentClass() as Class<PreferenceFragmentCompat>
         val builder = StringBuilder()
-        launchFragmentScenario(clazz).use {
-            it.onFragment { fragment ->
-                taskFinished.set(true)
-                fragment.preferenceScreen.toString(builder)
-            }
+        launchFragment(clazz) { fragment ->
+            taskFinished.set(true)
+            fragment.preferenceScreen.toString(builder)
         }
         return builder.toString()
     }
 
+    protected open fun launchFragment(
+        fragmentClass: Class<PreferenceFragmentCompat>,
+        action: (PreferenceFragmentCompat) -> Unit,
+    ): Unit = launchFragmentScenario(fragmentClass).use { it.onFragment(action) }
+
     protected open fun launchFragmentScenario(fragmentClass: Class<PreferenceFragmentCompat>) =
         FragmentScenario.launch(fragmentClass)
 
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 9726ecf..510c9b7 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -1139,11 +1139,5 @@
                 android:name="android.service.dream"
                 android:resource="@xml/home_controls_dream_metadata" />
         </service>
-
-        <activity android:name="com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity"
-            android:exported="false"
-            android:showForAllUsers="true"
-            android:theme="@style/ShortcutHelperTheme"
-            android:excludeFromRecents="true" />
     </application>
 </manifest>
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java
index 94f8846..0b15d23 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java
@@ -22,8 +22,14 @@
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 
+import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.wm.shell.shared.TransitionUtil.isClosingMode;
+import static com.android.wm.shell.shared.TransitionUtil.isClosingType;
+import static com.android.wm.shell.shared.TransitionUtil.isOpeningMode;
+
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.ArrayMap;
@@ -157,6 +163,9 @@
                         t.show(wallpapers[i].leash);
                         t.setAlpha(wallpapers[i].leash, 1.f);
                     }
+                    if (ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS.isTrue()) {
+                        resetLauncherAlphaOnDesktopExit(info, launcherTask, leashMap, t);
+                    }
                 } else {
                     if (launcherTask != null) {
                         counterLauncher.addChild(t, leashMap.get(launcherTask.getLeash()));
@@ -236,4 +245,33 @@
             }
         };
     }
+
+    /**
+     * Reset the alpha of the Launcher leash to give the Launcher time to hide its Views before the
+     * exit-desktop animation starts.
+     *
+     * This method should only be called if the current transition is opening Launcher, otherwise we
+     * might not be exiting Desktop Mode.
+     */
+    private static void resetLauncherAlphaOnDesktopExit(
+            TransitionInfo info,
+            TransitionInfo.Change launcherChange,
+            ArrayMap<SurfaceControl, SurfaceControl> leashMap,
+            SurfaceControl.Transaction startTransaction
+    ) {
+        checkArgument(isOpeningMode(launcherChange.getMode()));
+        if (!isClosingType(info.getType())) {
+            return;
+        }
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+            // skip changes that we didn't wrap
+            if (!leashMap.containsKey(change.getLeash())) continue;
+            // Only make the update if we are closing Desktop tasks.
+            if (change.getTaskInfo().isFreeform() && isClosingMode(change.getMode())) {
+                startTransaction.setAlpha(leashMap.get(launcherChange.getLeash()), 0f);
+                return;
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
index 025c8b9..f426aa5 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
@@ -70,6 +70,19 @@
             }
         """
 
+        private const val SIMPLEX_SIMPLE_SHADER =
+            """
+            vec4 main(vec2 p) {
+                vec2 uv = p / in_size.xy;
+                uv.x *= in_aspectRatio;
+
+                // Compute turbulence effect with the uv distorted with simplex noise.
+                vec3 noisePos = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum;
+                float mixFactor = simplex3d(noisePos) * 0.5 + 0.5;
+                return mix(in_color, in_screenColor, mixFactor);
+            }
+        """
+
         private const val FRACTAL_SHADER =
             """
             vec4 main(vec2 p) {
@@ -155,6 +168,8 @@
                 return sparkleLayer;
             }
         """
+        private const val SIMPLEX_NOISE_SIMPLE_SHADER =
+            ShaderUtilLibrary.SHADER_LIB + UNIFORMS + SIMPLEX_SIMPLE_SHADER
         private const val SIMPLEX_NOISE_SHADER =
             ShaderUtilLibrary.SHADER_LIB + UNIFORMS + COMMON_FUNCTIONS + SIMPLEX_SHADER
         private const val FRACTAL_NOISE_SHADER =
@@ -163,17 +178,20 @@
             ShaderUtilLibrary.SHADER_LIB + UNIFORMS + COMMON_FUNCTIONS + SIMPLEX_SPARKLE_SHADER
 
         enum class Type {
-            /** Effect with a simple color noise turbulence. */
+            /** Effect with a color noise turbulence with luma matte. */
             SIMPLEX_NOISE,
+            /** Effect with a noise interpolating between foreground and background colors. */
+            SIMPLEX_NOISE_SIMPLE,
             /** Effect with a simple color noise turbulence, with fractal. */
             SIMPLEX_NOISE_FRACTAL,
             /** Effect with color & sparkle turbulence with screen color layer. */
-            SIMPLEX_NOISE_SPARKLE
+            SIMPLEX_NOISE_SPARKLE,
         }
 
         fun getShader(type: Type): String {
             return when (type) {
                 Type.SIMPLEX_NOISE -> SIMPLEX_NOISE_SHADER
+                Type.SIMPLEX_NOISE_SIMPLE -> SIMPLEX_NOISE_SIMPLE_SHADER
                 Type.SIMPLEX_NOISE_FRACTAL -> FRACTAL_NOISE_SHADER
                 Type.SIMPLEX_NOISE_SPARKLE -> SPARKLE_NOISE_SHADER
             }
@@ -206,15 +224,15 @@
         setFloatUniform("in_pixelDensity", pixelDensity)
     }
 
-    /** Sets the noise color of the effect. Alpha is ignored. */
+    /**
+     * Sets the noise color of the effect. Alpha is ignored for all types except
+     * [Type.SIMPLEX_NOISE_SIMPLE].
+     */
     fun setColor(color: Int) {
         setColorUniform("in_color", color)
     }
 
-    /**
-     * Sets the color that is used for blending on top of the background color/image. Only relevant
-     * to [Type.SIMPLEX_NOISE_SPARKLE].
-     */
+    /** Sets the color that is used for blending on top of the background color/image. */
     fun setScreenColor(color: Int) {
         setColorUniform("in_screenColor", color)
     }
@@ -259,7 +277,7 @@
      */
     fun setLumaMatteFactors(
         lumaMatteBlendFactor: Float = 1f,
-        lumaMatteOverallBrightness: Float = 0f
+        lumaMatteOverallBrightness: Float = 0f,
     ) {
         setFloatUniform("in_lumaMatteBlendFactor", lumaMatteBlendFactor)
         setFloatUniform("in_lumaMatteOverallBrightness", lumaMatteOverallBrightness)
@@ -279,8 +297,10 @@
     /** Current noise movements in x, y, and z axes. */
     var noiseOffsetX: Float = 0f
         private set
+
     var noiseOffsetY: Float = 0f
         private set
+
     var noiseOffsetZ: Float = 0f
         private set
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
index c1c3b1f..d08df26 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
@@ -371,10 +371,7 @@
                 .motionTestValues { animatedAlpha(animatedOffset) exportAs MotionTestValues.alpha }
         }
 
-        UserSwitcher(
-            viewModel = viewModel,
-            modifier = Modifier.weight(1f).swappable().testTag("UserSwitcher"),
-        )
+        UserSwitcher(viewModel = viewModel, modifier = Modifier.weight(1f).swappable())
 
         FoldAware(
             modifier = Modifier.weight(1f).swappable(inversed = true).testTag("FoldAware"),
@@ -738,7 +735,7 @@
     Column(
         horizontalAlignment = Alignment.CenterHorizontally,
         verticalArrangement = Arrangement.Center,
-        modifier = modifier,
+        modifier = modifier.sysuiResTag("UserSwitcher"),
     ) {
         selectedUserImage?.let {
             Image(
@@ -781,7 +778,7 @@
                     Icon(
                         imageVector = Icons.Default.KeyboardArrowDown,
                         contentDescription = null,
-                        modifier = Modifier.size(32.dp),
+                        modifier = Modifier.size(32.dp).sysuiResTag("user_switcher_anchor"),
                     )
                 }
 
@@ -819,11 +816,11 @@
             expanded = isExpanded,
             onDismissRequest = onDismissed,
             offset = DpOffset(x = 0.dp, y = -UserSwitcherDropdownHeight),
-            modifier =
-                Modifier.width(UserSwitcherDropdownWidth).sysuiResTag("user_switcher_dropdown"),
+            modifier = Modifier.width(UserSwitcherDropdownWidth).sysuiResTag("user_list_dropdown"),
         ) {
             items.forEach { userSwitcherDropdownItem ->
                 DropdownMenuItem(
+                    modifier = Modifier.sysuiResTag("user_switcher_item"),
                     leadingIcon = {
                         Icon(
                             icon = userSwitcherDropdownItem.icon,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
index 3a9587c..521330f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
@@ -32,7 +32,9 @@
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
@@ -49,6 +51,8 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.util.fastIsFinite
+import androidx.compose.ui.zIndex
+import com.android.compose.modifiers.thenIf
 import com.android.systemui.communal.ui.viewmodel.DragHandle
 import com.android.systemui.communal.ui.viewmodel.ResizeInfo
 import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel
@@ -150,10 +154,9 @@
 /**
  * Draws a frame around the content with drag handles on the top and bottom of the content.
  *
- * @param index The index of this item in the [LazyGridState].
+ * @param key The unique key of this element, must be the same key used in the [LazyGridState].
+ * @param currentSpan The current span size of this item in the grid.
  * @param gridState The [LazyGridState] for the grid containing this item.
- * @param minItemSpan The minimum span that an item may occupy. Items are resized in multiples of
- *   this span.
  * @param gridContentPadding The content padding used for the grid, needed for determining offsets.
  * @param verticalArrangement The vertical arrangement of the grid items.
  * @param modifier Optional modifier to apply to the frame.
@@ -162,6 +165,10 @@
  * @param outlineColor Optional color to make the outline around the content.
  * @param cornerRadius Optional radius to give to the outline around the content.
  * @param strokeWidth Optional stroke width to draw the outline with.
+ * @param minHeightPx Optional minimum height in pixels that this widget can be resized to.
+ * @param maxHeightPx Optional maximum height in pixels that this widget can be resized to.
+ * @param resizeMultiple Optional number of spans that we allow resizing by. For example, if set to
+ *   3, then we only allow resizing in multiples of 3 spans.
  * @param alpha Optional function to provide an alpha value for the outline. Can be used to fade the
  *   outline in and out. This is wrapped in a function for performance, as the value is only
  *   accessed during the draw phase.
@@ -196,10 +203,19 @@
         }
 
     val dragHandleHeight = verticalArrangement.spacing - outlinePadding * 2
+    val isDragging by
+        remember(viewModel) {
+            derivedStateOf {
+                val topOffset = viewModel.topDragState.offset.takeIf { it.fastIsFinite() } ?: 0f
+                val bottomOffset =
+                    viewModel.bottomDragState.offset.takeIf { it.fastIsFinite() } ?: 0f
+                topOffset > 0 || bottomOffset > 0
+            }
+        }
 
     // Draw content surrounded by drag handles at top and bottom. Allow drag handles
     // to overlap content.
-    Box(modifier) {
+    Box(modifier.thenIf(isDragging) { Modifier.zIndex(1f) }) {
         content()
 
         if (enabled) {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index a0fed90..339445e 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -22,7 +22,7 @@
 import androidx.compose.material3.Text
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
@@ -294,15 +294,10 @@
             available: Offset,
             consumedByScroll: Offset = Offset.Zero,
         ) {
-            val consumedByPreScroll =
-                onPreScroll(available = available, source = NestedScrollSource.Drag)
+            val consumedByPreScroll = onPreScroll(available = available, source = UserInput)
             val consumed = consumedByPreScroll + consumedByScroll
 
-            onPostScroll(
-                consumed = consumed,
-                available = available - consumed,
-                source = NestedScrollSource.Drag,
-            )
+            onPostScroll(consumed = consumed, available = available - consumed, source = UserInput)
         }
 
         fun NestedScrollConnection.preFling(
@@ -738,7 +733,7 @@
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview)
         nestedScroll.onPreScroll(
             available = downOffset(fractionOfScreen = 0.1f),
-            source = NestedScrollSource.Drag,
+            source = UserInput,
         )
         assertIdle(currentScene = SceneA)
     }
@@ -750,7 +745,7 @@
             nestedScroll.onPostScroll(
                 consumed = Offset.Zero,
                 available = Offset.Zero,
-                source = NestedScrollSource.Drag,
+                source = UserInput,
             )
 
         assertIdle(currentScene = SceneA)
@@ -764,7 +759,7 @@
             nestedScroll.onPostScroll(
                 consumed = Offset.Zero,
                 available = downOffset(fractionOfScreen = 0.1f),
-                source = NestedScrollSource.Drag,
+                source = UserInput,
             )
 
         assertTransition(currentScene = SceneA)
@@ -784,16 +779,12 @@
         val consumed =
             nestedScroll.onPreScroll(
                 available = downOffset(fractionOfScreen = 0.1f),
-                source = NestedScrollSource.Drag,
+                source = UserInput,
             )
         assertThat(progress).isEqualTo(0.2f)
 
         // do nothing on postScroll
-        nestedScroll.onPostScroll(
-            consumed = consumed,
-            available = Offset.Zero,
-            source = NestedScrollSource.Drag,
-        )
+        nestedScroll.onPostScroll(consumed = consumed, available = Offset.Zero, source = UserInput)
         assertThat(progress).isEqualTo(0.2f)
 
         nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f))
@@ -813,10 +804,7 @@
         nestedScroll.preFling(available = Velocity.Zero)
 
         // a pre scroll event, that could be intercepted by DraggableHandlerImpl
-        nestedScroll.onPreScroll(
-            available = Offset(0f, secondScroll),
-            source = NestedScrollSource.Drag,
-        )
+        nestedScroll.onPreScroll(available = Offset(0f, secondScroll), source = UserInput)
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt
similarity index 72%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt
index f8e2f47..d1431ee 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt
@@ -16,25 +16,26 @@
 
 package com.android.systemui.keyboard.shortcut.ui
 
-import android.content.Intent
+import androidx.test.annotation.UiThreadTest
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource
 import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts
-import com.android.systemui.keyboard.shortcut.fakeShortcutHelperStartActivity
-import com.android.systemui.keyboard.shortcut.shortcutHelperActivityStarter
 import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
-import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity
+import com.android.systemui.keyboard.shortcut.shortcutHelperViewModel
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testCase
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.statusbar.phone.systemUIDialogFactory
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -46,7 +47,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class ShortcutHelperActivityStarterTest : SysuiTestCase() {
+class ShortcutHelperDialogStarterTest : SysuiTestCase() {
 
     private val fakeSystemSource = FakeKeyboardShortcutGroupsSource()
     private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource()
@@ -64,8 +65,14 @@
 
     private val testScope = kosmos.testScope
     private val testHelper = kosmos.shortcutHelperTestHelper
-    private val fakeStartActivity = kosmos.fakeShortcutHelperStartActivity
-    private val starter = kosmos.shortcutHelperActivityStarter
+    private val dialogFactory = kosmos.systemUIDialogFactory
+    private val coroutineScope = kosmos.applicationCoroutineScope
+    private val viewModel = kosmos.shortcutHelperViewModel
+
+    private val starter: ShortcutHelperDialogStarter =
+        with(kosmos) {
+            ShortcutHelperDialogStarter(coroutineScope, viewModel, dialogFactory, activityStarter)
+        }
 
     @Before
     fun setUp() {
@@ -74,21 +81,22 @@
     }
 
     @Test
-    fun start_doesNotStartByDefault() =
+    fun start_doesNotShowDialogByDefault() =
         testScope.runTest {
             starter.start()
 
-            assertThat(fakeStartActivity.startIntents).isEmpty()
+            assertThat(starter.dialog).isNull()
         }
 
     @Test
-    fun start_onToggle_startsActivity() =
+    @UiThreadTest
+    fun start_onToggle_showsDialog() =
         testScope.runTest {
             starter.start()
 
             testHelper.toggle(deviceId = 456)
 
-            verifyShortcutHelperActivityStarted()
+            assertThat(starter.dialog?.isShowing).isTrue()
         }
 
     @Test
@@ -101,34 +109,18 @@
 
             testHelper.toggle(deviceId = 456)
 
-            assertThat(fakeStartActivity.startIntents).isEmpty()
+            assertThat(starter.dialog).isNull()
         }
 
     @Test
-    fun start_onToggle_multipleTimesStartsActivityOnlyWhenNotStarted() =
-        testScope.runTest {
-            starter.start()
-
-            // Starts
-            testHelper.toggle(deviceId = 456)
-            // Stops
-            testHelper.toggle(deviceId = 456)
-            // Starts again
-            testHelper.toggle(deviceId = 456)
-            // Stops
-            testHelper.toggle(deviceId = 456)
-
-            verifyShortcutHelperActivityStarted(numTimes = 2)
-        }
-
-    @Test
+    @UiThreadTest
     fun start_onRequestShowShortcuts_startsActivity() =
         testScope.runTest {
             starter.start()
 
             testHelper.showFromActivity()
 
-            verifyShortcutHelperActivityStarted()
+            assertThat(starter.dialog?.isShowing).isTrue()
         }
 
     @Test
@@ -140,10 +132,11 @@
 
             testHelper.showFromActivity()
 
-            assertThat(fakeStartActivity.startIntents).isEmpty()
+            assertThat(starter.dialog).isNull()
         }
 
     @Test
+    @UiThreadTest
     fun start_onRequestShowShortcuts_multipleTimes_startsActivityOnlyOnce() =
         testScope.runTest {
             starter.start()
@@ -152,40 +145,40 @@
             testHelper.showFromActivity()
             testHelper.showFromActivity()
 
-            verifyShortcutHelperActivityStarted(numTimes = 1)
+            assertThat(starter.dialog?.isShowing).isTrue()
         }
 
     @Test
+    @UiThreadTest
     fun start_onRequestShowShortcuts_multipleTimes_startsActivityOnlyWhenNotStarted() =
         testScope.runTest {
             starter.start()
 
+            assertThat(starter.dialog).isNull()
             // No-op. Already hidden.
             testHelper.hideFromActivity()
+            assertThat(starter.dialog).isNull()
             // No-op. Already hidden.
             testHelper.hideForSystem()
+            assertThat(starter.dialog).isNull()
             // Show 1st time.
             testHelper.toggle(deviceId = 987)
+            assertThat(starter.dialog).isNotNull()
+            assertThat(starter.dialog?.isShowing).isTrue()
             // No-op. Already shown.
             testHelper.showFromActivity()
+            assertThat(starter.dialog?.isShowing).isTrue()
             // Hidden.
             testHelper.hideFromActivity()
+            assertThat(starter.dialog?.isShowing).isFalse()
             // No-op. Already hidden.
             testHelper.hideForSystem()
+            assertThat(starter.dialog?.isShowing).isFalse()
             // Show 2nd time.
             testHelper.toggle(deviceId = 456)
+            assertThat(starter.dialog?.isShowing).isTrue()
             // No-op. Already shown.
             testHelper.showFromActivity()
-
-            verifyShortcutHelperActivityStarted(numTimes = 2)
+            assertThat(starter.dialog?.isShowing).isTrue()
         }
-
-    private fun verifyShortcutHelperActivityStarted(numTimes: Int = 1) {
-        assertThat(fakeStartActivity.startIntents).hasSize(numTimes)
-        fakeStartActivity.startIntents.forEach { intent ->
-            assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
-            assertThat(intent.filterEquals(Intent(context, ShortcutHelperActivity::class.java)))
-                .isTrue()
-        }
-    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
index a629b24..5f3668a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
@@ -30,7 +30,6 @@
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
 import com.android.systemui.statusbar.window.StatusBarWindowController
-import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.capture
@@ -50,11 +49,10 @@
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 class SystemEventChipAnimationControllerTest : SysuiTestCase() {
-    private lateinit var controller: SystemEventChipAnimationController
+    private lateinit var controller: SystemEventChipAnimationControllerImpl
 
     @get:Rule val animatorTestRule = AnimatorTestRule(this)
     @Mock private lateinit var sbWindowController: StatusBarWindowController
-    @Mock private lateinit var sbWindowControllerStore: StatusBarWindowControllerStore
     @Mock private lateinit var insetsProvider: StatusBarContentInsetsProvider
 
     private var testView = TestView(mContext)
@@ -63,7 +61,6 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        whenever(sbWindowControllerStore.defaultDisplay).thenReturn(sbWindowController)
         // StatusBarWindowController is mocked. The addViewToWindow function needs to be mocked to
         // ensure that the chip view is added to a parent view
         whenever(sbWindowController.addViewToWindow(any(), any())).then {
@@ -76,7 +73,7 @@
             )
             statusbarFake.addView(
                 it.arguments[0] as View,
-                it.arguments[1] as FrameLayout.LayoutParams
+                it.arguments[1] as FrameLayout.LayoutParams,
             )
         }
 
@@ -86,16 +83,16 @@
                     /* left= */ insets,
                     /* top= */ insets,
                     /* right= */ insets,
-                    /* bottom= */ 0
+                    /* bottom= */ 0,
                 )
             )
         whenever(insetsProvider.getStatusBarContentAreaForCurrentRotation())
             .thenReturn(portraitArea)
 
         controller =
-            SystemEventChipAnimationController(
+            SystemEventChipAnimationControllerImpl(
                 context = mContext,
-                statusBarWindowControllerStore = sbWindowControllerStore,
+                statusBarWindowController = sbWindowController,
                 contentInsetsProvider = insetsProvider,
             )
     }
@@ -156,10 +153,7 @@
             val lp = it.arguments[1] as FrameLayout.LayoutParams
             assertThat(lp.gravity and Gravity.VERTICAL_GRAVITY_MASK).isEqualTo(Gravity.TOP)
 
-            statusbarFake.addView(
-                it.arguments[0] as View,
-                lp,
-            )
+            statusbarFake.addView(it.arguments[0] as View, lp)
         }
 
         // GIVEN insets provider gives the correct content area
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt
index 97fa6eb..75479ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.notification
 
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -58,10 +58,7 @@
         contentHeight = COLLAPSED_CONTENT_HEIGHT
 
         val offsetConsumed =
-            scrollConnection.onPreScroll(
-                available = Offset(x = 0f, y = -1f),
-                source = NestedScrollSource.Drag,
-            )
+            scrollConnection.onPreScroll(available = Offset(x = 0f, y = -1f), source = UserInput)
 
         assertThat(offsetConsumed).isEqualTo(Offset.Zero)
         assertThat(isStarted).isEqualTo(false)
@@ -73,10 +70,7 @@
         scrimOffset = MIN_SCRIM_OFFSET
 
         val offsetConsumed =
-            scrollConnection.onPreScroll(
-                available = Offset(x = 0f, y = -1f),
-                source = NestedScrollSource.Drag,
-            )
+            scrollConnection.onPreScroll(available = Offset(x = 0f, y = -1f), source = UserInput)
 
         assertThat(offsetConsumed).isEqualTo(Offset.Zero)
         assertThat(isStarted).isEqualTo(false)
@@ -88,10 +82,7 @@
 
         val availableOffset = Offset(x = 0f, y = -1f)
         val offsetConsumed =
-            scrollConnection.onPreScroll(
-                available = availableOffset,
-                source = NestedScrollSource.Drag,
-            )
+            scrollConnection.onPreScroll(available = availableOffset, source = UserInput)
 
         assertThat(offsetConsumed).isEqualTo(availableOffset)
         assertThat(isStarted).isEqualTo(true)
@@ -105,10 +96,7 @@
         val availableOffset = Offset(x = 0f, y = -2f)
         val consumableOffset = Offset(x = 0f, y = -1f)
         val offsetConsumed =
-            scrollConnection.onPreScroll(
-                available = availableOffset,
-                source = NestedScrollSource.Drag,
-            )
+            scrollConnection.onPreScroll(available = availableOffset, source = UserInput)
 
         assertThat(offsetConsumed).isEqualTo(consumableOffset)
         assertThat(isStarted).isEqualTo(true)
@@ -120,7 +108,7 @@
             scrollConnection.onPostScroll(
                 consumed = Offset.Zero,
                 available = Offset(x = 0f, y = -1f),
-                source = NestedScrollSource.Drag,
+                source = UserInput,
             )
 
         assertThat(offsetConsumed).isEqualTo(Offset.Zero)
@@ -130,10 +118,7 @@
     @Test
     fun onScrollDown_canStartPreScroll_ignoreScroll() = runTest {
         val offsetConsumed =
-            scrollConnection.onPreScroll(
-                available = Offset(x = 0f, y = 1f),
-                source = NestedScrollSource.Drag,
-            )
+            scrollConnection.onPreScroll(available = Offset(x = 0f, y = 1f), source = UserInput)
 
         assertThat(offsetConsumed).isEqualTo(Offset.Zero)
         assertThat(isStarted).isEqualTo(false)
@@ -148,7 +133,7 @@
             scrollConnection.onPostScroll(
                 consumed = Offset.Zero,
                 available = availableOffset,
-                source = NestedScrollSource.Drag
+                source = UserInput,
             )
 
         assertThat(offsetConsumed).isEqualTo(availableOffset)
@@ -165,7 +150,7 @@
             scrollConnection.onPostScroll(
                 consumed = Offset.Zero,
                 available = availableOffset,
-                source = NestedScrollSource.Drag
+                source = UserInput,
             )
 
         assertThat(offsetConsumed).isEqualTo(consumableOffset)
@@ -180,7 +165,7 @@
             scrollConnection.onPostScroll(
                 consumed = Offset.Zero,
                 available = Offset(x = 0f, y = 1f),
-                source = NestedScrollSource.Drag
+                source = UserInput,
             )
 
         assertThat(offsetConsumed).isEqualTo(Offset.Zero)
@@ -197,7 +182,7 @@
             scrollConnection.onPostScroll(
                 consumed = Offset.Zero,
                 available = Offset(x = 0f, y = 1f),
-                source = NestedScrollSource.Drag
+                source = UserInput,
             )
 
         assertThat(offsetConsumed).isEqualTo(Offset.Zero)
@@ -210,17 +195,11 @@
     fun canContinueScroll_inBetweenMinMaxOffset_true() = runTest {
         scrimOffset = (MIN_SCRIM_OFFSET + MAX_SCRIM_OFFSET) / 2f
         contentHeight = EXPANDED_CONTENT_HEIGHT
-        scrollConnection.onPreScroll(
-            available = Offset(x = 0f, y = -1f),
-            source = NestedScrollSource.Drag
-        )
+        scrollConnection.onPreScroll(available = Offset(x = 0f, y = -1f), source = UserInput)
 
         assertThat(isStarted).isEqualTo(true)
 
-        scrollConnection.onPreScroll(
-            available = Offset(x = 0f, y = 1f),
-            source = NestedScrollSource.Drag
-        )
+        scrollConnection.onPreScroll(available = Offset(x = 0f, y = 1f), source = UserInput)
 
         assertThat(isStarted).isEqualTo(true)
     }
@@ -229,17 +208,11 @@
     fun canContinueScroll_atMaxOffset_false() = runTest {
         scrimOffset = MAX_SCRIM_OFFSET
         contentHeight = EXPANDED_CONTENT_HEIGHT
-        scrollConnection.onPreScroll(
-            available = Offset(x = 0f, y = -1f),
-            source = NestedScrollSource.Drag
-        )
+        scrollConnection.onPreScroll(available = Offset(x = 0f, y = -1f), source = UserInput)
 
         assertThat(isStarted).isEqualTo(true)
 
-        scrollConnection.onPreScroll(
-            available = Offset(x = 0f, y = 1f),
-            source = NestedScrollSource.Drag
-        )
+        scrollConnection.onPreScroll(available = Offset(x = 0f, y = 1f), source = UserInput)
 
         assertThat(isStarted).isEqualTo(false)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCacheTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCacheTest.kt
new file mode 100644
index 0000000..d2a3a19
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCacheTest.kt
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NotifCollectionCacheTest : SysuiTestCase() {
+    companion object {
+        const val A = "a"
+        const val B = "b"
+        const val C = "c"
+    }
+
+    val systemClock = FakeSystemClock()
+    val underTest =
+        NotifCollectionCache<String>(purgeTimeoutMillis = 200L, systemClock = systemClock)
+
+    @After
+    fun cleanUp() {
+        underTest.clear()
+    }
+
+    @Test
+    fun fetch_isOnlyCalledOncePerEntry() {
+        val fetchList = mutableListOf<String>()
+        val fetch = { key: String ->
+            fetchList.add(key)
+            key
+        }
+
+        // Construct the cache and make sure fetch is called
+        assertThat(underTest.getOrFetch(A, fetch)).isEqualTo(A)
+        assertThat(underTest.getOrFetch(B, fetch)).isEqualTo(B)
+        assertThat(underTest.getOrFetch(C, fetch)).isEqualTo(C)
+        assertThat(fetchList).containsExactly(A, B, C).inOrder()
+
+        // Verify that further calls don't trigger fetch again
+        underTest.getOrFetch(A, fetch)
+        underTest.getOrFetch(A, fetch)
+        underTest.getOrFetch(B, fetch)
+        underTest.getOrFetch(C, fetch)
+        assertThat(fetchList).containsExactly(A, B, C).inOrder()
+
+        // Verify that fetch gets called again if the entries are cleared
+        underTest.clear()
+        underTest.getOrFetch(A, fetch)
+        assertThat(fetchList).containsExactly(A, B, C, A).inOrder()
+    }
+
+    @Test
+    fun purge_beforeTimeout_doesNothing() {
+        // Populate cache
+        val fetch = { key: String -> key }
+        underTest.getOrFetch(A, fetch)
+        underTest.getOrFetch(B, fetch)
+        underTest.getOrFetch(C, fetch)
+
+        // B starts off with ♥ ︎♥︎
+        assertThat(underTest.getLives(B)).isEqualTo(2)
+        // First purge run removes a ︎♥︎
+        underTest.purge(listOf(A, C))
+        assertNotNull(underTest.cache[B])
+        assertThat(underTest.getLives(B)).isEqualTo(1)
+        // Second purge run done too early does nothing to B
+        systemClock.advanceTime(100L)
+        underTest.purge(listOf(A, C))
+        assertNotNull(underTest.cache[B])
+        assertThat(underTest.getLives(B)).isEqualTo(1)
+        // Purge done after timeout (200ms) clears B
+        systemClock.advanceTime(100L)
+        underTest.purge(listOf(A, C))
+        assertNull(underTest.cache[B])
+    }
+
+    @Test
+    fun get_resetsLives() {
+        // Populate cache
+        val fetch = { key: String -> key }
+        underTest.getOrFetch(A, fetch)
+        underTest.getOrFetch(B, fetch)
+        underTest.getOrFetch(C, fetch)
+
+        // Bring B down to one ︎♥︎
+        underTest.purge(listOf(A, C))
+        assertThat(underTest.getLives(B)).isEqualTo(1)
+
+        // Get should restore B to ♥ ︎♥︎
+        underTest.getOrFetch(B, fetch)
+        assertThat(underTest.getLives(B)).isEqualTo(2)
+
+        // Subsequent purge should remove a life regardless of timing
+        underTest.purge(listOf(A, C))
+        assertThat(underTest.getLives(B)).isEqualTo(1)
+    }
+
+    @Test
+    fun purge_resetsLives() {
+        // Populate cache
+        val fetch = { key: String -> key }
+        underTest.getOrFetch(A, fetch)
+        underTest.getOrFetch(B, fetch)
+        underTest.getOrFetch(C, fetch)
+
+        // Bring B down to one ︎♥︎
+        underTest.purge(listOf(A, C))
+        assertThat(underTest.getLives(B)).isEqualTo(1)
+
+        // When B is back to wantedKeys, it is restored to to ♥ ︎♥ ︎︎
+        underTest.purge(listOf(B))
+        assertThat(underTest.getLives(B)).isEqualTo(2)
+        assertThat(underTest.getLives(A)).isEqualTo(1)
+        assertThat(underTest.getLives(C)).isEqualTo(1)
+
+        // Subsequent purge should remove a life regardless of timing
+        underTest.purge(listOf(A, C))
+        assertThat(underTest.getLives(B)).isEqualTo(1)
+    }
+
+    @Test
+    fun purge_worksWithMoreLives() {
+        val multiLivesCache =
+            NotifCollectionCache<String>(
+                retainCount = 3,
+                purgeTimeoutMillis = 100L,
+                systemClock = systemClock,
+            )
+
+        // Populate cache
+        val fetch = { key: String -> key }
+        multiLivesCache.getOrFetch(A, fetch)
+        multiLivesCache.getOrFetch(B, fetch)
+        multiLivesCache.getOrFetch(C, fetch)
+
+        // B starts off with ♥ ︎♥︎ ♥ ︎♥︎
+        assertThat(multiLivesCache.getLives(B)).isEqualTo(4)
+        // First purge run removes a ︎♥︎
+        multiLivesCache.purge(listOf(A, C))
+        assertNotNull(multiLivesCache.cache[B])
+        assertThat(multiLivesCache.getLives(B)).isEqualTo(3)
+        // Second purge run done too early does nothing to B
+        multiLivesCache.purge(listOf(A, C))
+        assertNotNull(multiLivesCache.cache[B])
+        assertThat(multiLivesCache.getLives(B)).isEqualTo(3)
+        // Staggered purge runs remove further ︎♥︎
+        systemClock.advanceTime(100L)
+        multiLivesCache.purge(listOf(A, C))
+        assertNotNull(multiLivesCache.cache[B])
+        assertThat(multiLivesCache.getLives(B)).isEqualTo(2)
+        systemClock.advanceTime(100L)
+        multiLivesCache.purge(listOf(A, C))
+        assertNotNull(multiLivesCache.cache[B])
+        assertThat(multiLivesCache.getLives(B)).isEqualTo(1)
+        systemClock.advanceTime(100L)
+        multiLivesCache.purge(listOf(A, C))
+        assertNull(multiLivesCache.cache[B])
+    }
+
+    @Test
+    fun purge_worksWithNoLives() {
+        val noLivesCache =
+            NotifCollectionCache<String>(
+                retainCount = 0,
+                purgeTimeoutMillis = 0L,
+                systemClock = systemClock,
+            )
+
+        val fetch = { key: String -> key }
+        noLivesCache.getOrFetch(A, fetch)
+        noLivesCache.getOrFetch(B, fetch)
+        noLivesCache.getOrFetch(C, fetch)
+
+        // Purge immediately removes entry
+        noLivesCache.purge(listOf(A, C))
+
+        assertNotNull(noLivesCache.cache[A])
+        assertNull(noLivesCache.cache[B])
+        assertNotNull(noLivesCache.cache[C])
+    }
+
+    @Test
+    fun hitsAndMisses_areAccurate() {
+        val fetch = { key: String -> key }
+
+        // Construct the cache
+        assertThat(underTest.getOrFetch(A, fetch)).isEqualTo(A)
+        assertThat(underTest.getOrFetch(B, fetch)).isEqualTo(B)
+        assertThat(underTest.getOrFetch(C, fetch)).isEqualTo(C)
+        assertThat(underTest.hits.get()).isEqualTo(0)
+        assertThat(underTest.misses.get()).isEqualTo(3)
+
+        // Verify that further calls count as hits
+        underTest.getOrFetch(A, fetch)
+        underTest.getOrFetch(A, fetch)
+        underTest.getOrFetch(B, fetch)
+        underTest.getOrFetch(C, fetch)
+        assertThat(underTest.hits.get()).isEqualTo(4)
+        assertThat(underTest.misses.get()).isEqualTo(3)
+
+        // Verify that a miss is counted again if the entries are cleared
+        underTest.clear()
+        underTest.getOrFetch(A, fetch)
+        assertThat(underTest.hits.get()).isEqualTo(4)
+        assertThat(underTest.misses.get()).isEqualTo(4)
+    }
+
+    private fun <V> NotifCollectionCache<V>.getLives(key: String) = this.cache[key]?.lives
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
index 9d990b1..9a6a699 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
@@ -26,6 +26,7 @@
 import com.android.internal.widget.MessagingGroup
 import com.android.internal.widget.MessagingImageMessage
 import com.android.internal.widget.MessagingLinearLayout
+import com.android.internal.widget.NotificationRowIconView
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper
@@ -90,7 +91,7 @@
 
     private fun fakeConversationLayout(
         mockDrawableGroupMessage: AnimatedImageDrawable,
-        mockDrawableImageMessage: AnimatedImageDrawable
+        mockDrawableImageMessage: AnimatedImageDrawable,
     ): View {
         val mockMessagingImageMessage: MessagingImageMessage =
             mock<MessagingImageMessage>().apply {
@@ -126,6 +127,7 @@
                 whenever(requireViewById<CachingIconView>(R.id.conversation_icon))
                     .thenReturn(mock())
                 whenever(findViewById<CachingIconView>(R.id.icon)).thenReturn(mock())
+                whenever(requireViewById<NotificationRowIconView>(R.id.icon)).thenReturn(mock())
                 whenever(requireViewById<View>(R.id.conversation_icon_badge_bg)).thenReturn(mock())
                 whenever(requireViewById<View>(R.id.expand_button)).thenReturn(mock())
                 whenever(requireViewById<View>(R.id.expand_button_container)).thenReturn(mock())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShaderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShaderTest.kt
index 286f017..dbe90a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShaderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShaderTest.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE
 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE_FRACTAL
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE_SIMPLE
 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE_SPARKLE
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -28,20 +29,23 @@
 @RunWith(AndroidJUnit4::class)
 class TurbulenceNoiseShaderTest : SysuiTestCase() {
 
-    private lateinit var turbulenceNoiseShader: TurbulenceNoiseShader
-
     @Test
     fun compilesSimplexNoise() {
-        turbulenceNoiseShader = TurbulenceNoiseShader(baseType = SIMPLEX_NOISE)
+        TurbulenceNoiseShader(baseType = SIMPLEX_NOISE)
+    }
+
+    @Test
+    fun compilesSimplexSimpleNoise() {
+        TurbulenceNoiseShader(baseType = SIMPLEX_NOISE_SIMPLE)
     }
 
     @Test
     fun compilesFractalNoise() {
-        turbulenceNoiseShader = TurbulenceNoiseShader(baseType = SIMPLEX_NOISE_FRACTAL)
+        TurbulenceNoiseShader(baseType = SIMPLEX_NOISE_FRACTAL)
     }
 
     @Test
     fun compilesSparkleNoise() {
-        turbulenceNoiseShader = TurbulenceNoiseShader(baseType = SIMPLEX_NOISE_SPARKLE)
+        TurbulenceNoiseShader(baseType = SIMPLEX_NOISE_SPARKLE)
     }
 }
diff --git a/packages/SystemUI/res/anim/shortcut_helper_close_anim.xml b/packages/SystemUI/res/anim/shortcut_helper_close_anim.xml
deleted file mode 100644
index 47fd78a..0000000
--- a/packages/SystemUI/res/anim/shortcut_helper_close_anim.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2024 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.
-  -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-    android:interpolator="@android:anim/accelerate_interpolator"
-    android:zAdjustment="top">
-
-    <translate
-        android:fromYDelta="0"
-        android:toYDelta="100%"
-        android:duration="@android:integer/config_shortAnimTime" />
-</set>
diff --git a/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml b/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml
deleted file mode 100644
index 77edf58..0000000
--- a/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2024 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.
-  -->
-
-<!-- Animation for when a dock window at the bottom of the screen is entering. -->
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
-    android:zAdjustment="top">
-
-    <translate android:fromYDelta="100%"
-        android:toYDelta="0"
-        android:startOffset="@android:integer/config_shortAnimTime"
-        android:duration="@android:integer/config_mediumAnimTime"/>
-</set>
diff --git a/packages/SystemUI/res/values-night/styles.xml b/packages/SystemUI/res/values-night/styles.xml
index 17ba2e5..d5d52e3 100644
--- a/packages/SystemUI/res/values-night/styles.xml
+++ b/packages/SystemUI/res/values-night/styles.xml
@@ -59,8 +59,4 @@
         -->
         <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
     </style>
-
-    <style name="ShortcutHelperTheme" parent="@style/ShortcutHelperThemeCommon">
-        <item name="android:windowLightNavigationBar">false</item>
-    </style>
 </resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 7cebac2..bda3453 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1709,32 +1709,4 @@
     <style name="Theme.SystemUI.Dialog.StickyKeys" parent="@style/Theme.SystemUI.Dialog">
         <item name="android:colorBackground">@color/transparent</item>
     </style>
-
-    <style name="ShortcutHelperBottomSheet" parent="@style/Widget.Material3.BottomSheet">
-        <item name="backgroundTint">?colorSurfaceContainer</item>
-    </style>
-
-    <style name="ShortcutHelperAnimation" parent="@android:style/Animation.Activity">
-        <item name="android:activityOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item>
-        <item name="android:taskOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item>
-        <item name="android:activityOpenExitAnimation">@anim/shortcut_helper_close_anim</item>
-        <item name="android:taskOpenExitAnimation">@anim/shortcut_helper_close_anim</item>
-    </style>
-
-    <style name="ShortcutHelperThemeCommon" parent="@style/Theme.Material3.DynamicColors.DayNight">
-        <item name="android:windowAnimationStyle">@style/ShortcutHelperAnimation</item>
-        <item name="android:windowIsTranslucent">true</item>
-        <item name="android:windowNoTitle">true</item>
-        <item name="android:windowBackground">@android:color/transparent</item>
-        <item name="android:backgroundDimEnabled">true</item>
-        <item name="android:statusBarColor">@android:color/transparent</item>
-        <item name="android:windowContentOverlay">@null</item>
-        <item name="android:navigationBarColor">@android:color/transparent</item>
-        <item name="android:windowLayoutInDisplayCutoutMode">always</item>
-        <item name="enableEdgeToEdge">true</item>
-    </style>
-
-    <style name="ShortcutHelperTheme" parent="@style/ShortcutHelperThemeCommon">
-        <item name="android:windowLightNavigationBar">true</item>
-    </style>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/CameraProtectionLoader.kt b/packages/SystemUI/src/com/android/systemui/CameraProtectionLoader.kt
index 6cee28b..0c29466 100644
--- a/packages/SystemUI/src/com/android/systemui/CameraProtectionLoader.kt
+++ b/packages/SystemUI/src/com/android/systemui/CameraProtectionLoader.kt
@@ -22,15 +22,18 @@
 import android.graphics.RectF
 import android.util.PathParser
 import com.android.systemui.res.R
-import javax.inject.Inject
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 import kotlin.math.roundToInt
 
 interface CameraProtectionLoader {
     fun loadCameraProtectionInfoList(): List<CameraProtectionInfo>
 }
 
-class CameraProtectionLoaderImpl @Inject constructor(private val context: Context) :
-    CameraProtectionLoader {
+class CameraProtectionLoaderImpl
+@AssistedInject
+constructor(@Assisted private val context: Context) : CameraProtectionLoader {
 
     override fun loadCameraProtectionInfoList(): List<CameraProtectionInfo> {
         val list = mutableListOf<CameraProtectionInfo>()
@@ -76,7 +79,7 @@
                 computed.left.roundToInt(),
                 computed.top.roundToInt(),
                 computed.right.roundToInt(),
-                computed.bottom.roundToInt()
+                computed.bottom.roundToInt(),
             )
         val displayUniqueId = context.getString(displayUniqueIdRes)
         return CameraProtectionInfo(
@@ -84,7 +87,7 @@
             physicalCameraId,
             protectionPath,
             protectionBounds,
-            displayUniqueId
+            displayUniqueId,
         )
     }
 
@@ -95,4 +98,9 @@
             throw IllegalArgumentException("Invalid protection path", e)
         }
     }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(context: Context): CameraProtectionLoaderImpl
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/CameraProtectionModule.kt b/packages/SystemUI/src/com/android/systemui/CameraProtectionModule.kt
index 58680a8..442a1e4 100644
--- a/packages/SystemUI/src/com/android/systemui/CameraProtectionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/CameraProtectionModule.kt
@@ -16,11 +16,20 @@
 
 package com.android.systemui
 
-import dagger.Binds
+import android.content.Context
+import com.android.systemui.dagger.SysUISingleton
 import dagger.Module
+import dagger.Provides
 
 @Module
-interface CameraProtectionModule {
+object CameraProtectionModule {
 
-    @Binds fun cameraProtectionLoaderImpl(impl: CameraProtectionLoaderImpl): CameraProtectionLoader
+    @Provides
+    @SysUISingleton
+    fun cameraProtectionLoader(
+        factory: CameraProtectionLoaderImpl.Factory,
+        context: Context,
+    ): CameraProtectionLoader {
+        return factory.create(context)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorationsModule.kt b/packages/SystemUI/src/com/android/systemui/ScreenDecorationsModule.kt
index 044312b..6fc50fb 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorationsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorationsModule.kt
@@ -16,9 +16,14 @@
 
 package com.android.systemui
 
+import android.content.Context
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.decor.FaceScanningProviderFactory
+import com.android.systemui.decor.FaceScanningProviderFactoryImpl
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
 import dagger.Binds
 import dagger.Module
+import dagger.Provides
 import dagger.multibindings.ClassKey
 import dagger.multibindings.IntoMap
 import dagger.multibindings.IntoSet
@@ -35,4 +40,15 @@
     @Binds
     @IntoSet
     fun bindScreenDecorationsConfigListener(impl: ScreenDecorations): ConfigurationListener
+
+    companion object {
+        @Provides
+        @SysUISingleton
+        fun faceScanningProviderFactory(
+            creator: FaceScanningProviderFactoryImpl.Creator,
+            context: Context,
+        ): FaceScanningProviderFactory {
+            return creator.create(context)
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/SysUICutoutProvider.kt b/packages/SystemUI/src/com/android/systemui/SysUICutoutProvider.kt
index 7309599..b4cb103 100644
--- a/packages/SystemUI/src/com/android/systemui/SysUICutoutProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/SysUICutoutProvider.kt
@@ -21,21 +21,12 @@
 import android.util.RotationUtils
 import android.view.Display
 import android.view.DisplayCutout
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.display.naturalBounds
-import javax.inject.Inject
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 
-@SysUISingleton
-class SysUICutoutProvider
-@Inject
-constructor(
-    private val context: Context,
-    private val cameraProtectionLoader: CameraProtectionLoader,
-) {
-
-    private val cameraProtectionList by lazy {
-        cameraProtectionLoader.loadCameraProtectionInfoList()
-    }
+interface SysUICutoutProvider {
 
     /**
      * Returns the [SysUICutoutInformation] for the current display and the current rotation.
@@ -43,7 +34,21 @@
      * This means that the bounds of the display cutout and the camera protection will be
      * adjusted/rotated for the current rotation.
      */
-    fun cutoutInfoForCurrentDisplayAndRotation(): SysUICutoutInformation? {
+    fun cutoutInfoForCurrentDisplayAndRotation(): SysUICutoutInformation?
+}
+
+class SysUICutoutProviderImpl
+@AssistedInject
+constructor(
+    @Assisted private val context: Context,
+    @Assisted private val cameraProtectionLoader: CameraProtectionLoader,
+) : SysUICutoutProvider {
+
+    private val cameraProtectionList by lazy {
+        cameraProtectionLoader.loadCameraProtectionInfoList()
+    }
+
+    override fun cutoutInfoForCurrentDisplayAndRotation(): SysUICutoutInformation? {
         val display = context.display
         val displayCutout: DisplayCutout = display.cutout ?: return null
         return SysUICutoutInformation(displayCutout, getCameraProtectionForDisplay(display))
@@ -72,8 +77,16 @@
             /* inOutBounds = */ rotatedBoundsOut,
             /* parentWidth = */ displayNaturalBounds.width(),
             /* parentHeight = */ displayNaturalBounds.height(),
-            /* rotation = */ display.rotation
+            /* rotation = */ display.rotation,
         )
         return rotatedBoundsOut
     }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            context: Context,
+            cameraProtectionLoader: CameraProtectionLoader,
+        ): SysUICutoutProviderImpl
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt
index 50970a5..8b5fde3 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt
@@ -55,11 +55,7 @@
             settingsLibAudioSharingRepository: SettingsLibAudioSharingRepository,
             @Background backgroundDispatcher: CoroutineDispatcher,
         ): AudioSharingRepository =
-            if (
-                Flags.enableLeAudioSharing() &&
-                    Flags.audioSharingQsDialogImprovement() &&
-                    localBluetoothManager != null
-            ) {
+            if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
                 AudioSharingRepositoryImpl(
                     localBluetoothManager,
                     settingsLibAudioSharingRepository,
diff --git a/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderFactory.kt
index cbed21c..bfd6b5b 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderFactory.kt
@@ -22,18 +22,15 @@
 import android.view.DisplayCutout
 import android.view.DisplayInfo
 
-class CutoutDecorProviderFactory constructor(
-    private val res: Resources,
-    private val display: Display?,
-) : DecorProviderFactory() {
+class CutoutDecorProviderFactory(private val res: Resources, private val display: Display?) :
+    DecorProviderFactory {
 
     val displayInfo = DisplayInfo()
 
     override val hasProviders: Boolean
         get() {
-            display?.getDisplayInfo(displayInfo) ?: run {
-                Log.w(TAG, "display is null, can't update displayInfo")
-            }
+            display?.getDisplayInfo(displayInfo)
+                ?: run { Log.w(TAG, "display is null, can't update displayInfo") }
             return DisplayCutout.getFillBuiltInDisplayCutout(res, displayInfo.uniqueId)
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/decor/DecorProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/DecorProviderFactory.kt
index c60cad8..16e73f5 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/DecorProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/DecorProviderFactory.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.decor
 
-abstract class DecorProviderFactory {
-    abstract val providers: List<DecorProvider>
-    abstract val hasProviders: Boolean
-}
\ No newline at end of file
+interface DecorProviderFactory {
+    val providers: List<DecorProvider>
+    val hasProviders: Boolean
+}
diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
index 3bc4f34..88580cf 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
@@ -33,23 +33,32 @@
 import com.android.systemui.FaceScanningOverlay
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.biometrics.data.repository.FacePropertyRepository
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.log.ScreenDecorationsLogger
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 import java.util.concurrent.Executor
-import javax.inject.Inject
 
-@SysUISingleton
-class FaceScanningProviderFactory @Inject constructor(
-        private val authController: AuthController,
-        private val context: Context,
-        private val statusBarStateController: StatusBarStateController,
-        private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-        @Main private val mainExecutor: Executor,
-        private val logger: ScreenDecorationsLogger,
-        private val facePropertyRepository: FacePropertyRepository,
-) : DecorProviderFactory() {
+interface FaceScanningProviderFactory : DecorProviderFactory {
+
+    fun canShowFaceScanningAnim(): Boolean
+
+    fun shouldShowFaceScanningAnim(): Boolean
+}
+
+class FaceScanningProviderFactoryImpl
+@AssistedInject
+constructor(
+    private val authController: AuthController,
+    @Assisted private val context: Context,
+    private val statusBarStateController: StatusBarStateController,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    @Main private val mainExecutor: Executor,
+    private val logger: ScreenDecorationsLogger,
+    private val facePropertyRepository: FacePropertyRepository,
+) : FaceScanningProviderFactory {
     private val display = context.display
     private val displayInfo = DisplayInfo()
 
@@ -60,11 +69,12 @@
             }
 
             // update display info
-            display?.getDisplayInfo(displayInfo) ?: run {
-                Log.w(TAG, "display is null, can't update displayInfo")
-            }
+            display?.getDisplayInfo(displayInfo)
+                ?: run { Log.w(TAG, "display is null, can't update displayInfo") }
             return DisplayCutout.getFillBuiltInDisplayCutout(
-                    context.resources, displayInfo.uniqueId)
+                context.resources,
+                displayInfo.uniqueId,
+            )
         }
 
     override val providers: List<DecorProvider>
@@ -81,39 +91,45 @@
                     // Cutout drawing is updated in ScreenDecorations#updateCutout
                     for (bound in bounds) {
                         list.add(
-                                FaceScanningOverlayProviderImpl(
-                                        bound.baseOnRotation0(displayInfo.rotation),
-                                        authController,
-                                        statusBarStateController,
-                                        keyguardUpdateMonitor,
-                                        mainExecutor,
-                                        logger,
-                                        facePropertyRepository,
-                                )
+                            FaceScanningOverlayProviderImpl(
+                                bound.baseOnRotation0(displayInfo.rotation),
+                                authController,
+                                statusBarStateController,
+                                keyguardUpdateMonitor,
+                                mainExecutor,
+                                logger,
+                                facePropertyRepository,
+                            )
                         )
                     }
                 }
             }
         }
 
-    fun canShowFaceScanningAnim(): Boolean {
+    override fun canShowFaceScanningAnim(): Boolean {
         return hasProviders && keyguardUpdateMonitor.isFaceEnabledAndEnrolled
     }
 
-    fun shouldShowFaceScanningAnim(): Boolean {
+    override fun shouldShowFaceScanningAnim(): Boolean {
         return canShowFaceScanningAnim() &&
-                (keyguardUpdateMonitor.isFaceDetectionRunning || authController.isShowing)
+            (keyguardUpdateMonitor.isFaceDetectionRunning || authController.isShowing)
+    }
+
+    // Using the name "Creator" so that it doesn't become "...FactoryFactory".
+    @AssistedFactory
+    interface Creator {
+        fun create(context: Context): FaceScanningProviderFactoryImpl
     }
 }
 
 class FaceScanningOverlayProviderImpl(
-        override val alignedBound: Int,
-        private val authController: AuthController,
-        private val statusBarStateController: StatusBarStateController,
-        private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-        private val mainExecutor: Executor,
-        private val logger: ScreenDecorationsLogger,
-        private val facePropertyRepository: FacePropertyRepository,
+    override val alignedBound: Int,
+    private val authController: AuthController,
+    private val statusBarStateController: StatusBarStateController,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val mainExecutor: Executor,
+    private val logger: ScreenDecorationsLogger,
+    private val facePropertyRepository: FacePropertyRepository,
 ) : BoundDecorProvider() {
     override val viewId: Int = com.android.systemui.res.R.id.face_scanning_anim
 
@@ -122,7 +138,7 @@
         reloadToken: Int,
         @Surface.Rotation rotation: Int,
         tintColor: Int,
-        displayUniqueId: String?
+        displayUniqueId: String?,
     ) {
         (view.layoutParams as FrameLayout.LayoutParams).let {
             updateLayoutParams(it, rotation)
@@ -138,9 +154,10 @@
         context: Context,
         parent: ViewGroup,
         @Surface.Rotation rotation: Int,
-        tintColor: Int
+        tintColor: Int,
     ): View {
-        val view = FaceScanningOverlay(
+        val view =
+            FaceScanningOverlay(
                 context,
                 alignedBound,
                 statusBarStateController,
@@ -148,43 +165,46 @@
                 mainExecutor,
                 logger,
                 authController,
-        )
+            )
         view.id = viewId
         view.setColor(tintColor)
-        FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                ViewGroup.LayoutParams.MATCH_PARENT).let {
-            updateLayoutParams(it, rotation)
-            parent.addView(view, it)
-        }
+        FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT,
+            )
+            .let {
+                updateLayoutParams(it, rotation)
+                parent.addView(view, it)
+            }
         return view
     }
 
     private fun updateLayoutParams(
         layoutParams: FrameLayout.LayoutParams,
-        @Surface.Rotation rotation: Int
+        @Surface.Rotation rotation: Int,
     ) {
         layoutParams.let { lp ->
             lp.width = ViewGroup.LayoutParams.MATCH_PARENT
             lp.height = ViewGroup.LayoutParams.MATCH_PARENT
             logger.faceSensorLocation(facePropertyRepository.sensorLocation.value)
-            facePropertyRepository.sensorLocation.value?.y?.let {
-                faceAuthSensorHeight ->
+            facePropertyRepository.sensorLocation.value?.y?.let { faceAuthSensorHeight ->
                 val faceScanningHeight = (faceAuthSensorHeight * 2)
                 when (rotation) {
-                    Surface.ROTATION_0, Surface.ROTATION_180 ->
-                        lp.height = faceScanningHeight
-                    Surface.ROTATION_90, Surface.ROTATION_270 ->
-                        lp.width = faceScanningHeight
+                    Surface.ROTATION_0,
+                    Surface.ROTATION_180 -> lp.height = faceScanningHeight
+                    Surface.ROTATION_90,
+                    Surface.ROTATION_270 -> lp.width = faceScanningHeight
                 }
             }
 
-            lp.gravity = when (rotation) {
-                Surface.ROTATION_0 -> Gravity.TOP or Gravity.START
-                Surface.ROTATION_90 -> Gravity.LEFT or Gravity.START
-                Surface.ROTATION_180 -> Gravity.BOTTOM or Gravity.END
-                Surface.ROTATION_270 -> Gravity.RIGHT or Gravity.END
-                else -> -1 /* invalid rotation */
-            }
+            lp.gravity =
+                when (rotation) {
+                    Surface.ROTATION_0 -> Gravity.TOP or Gravity.START
+                    Surface.ROTATION_90 -> Gravity.LEFT or Gravity.START
+                    Surface.ROTATION_180 -> Gravity.BOTTOM or Gravity.END
+                    Surface.ROTATION_270 -> Gravity.RIGHT or Gravity.END
+                    else -> -1 /* invalid rotation */
+                }
         }
     }
 }
@@ -209,24 +229,27 @@
 fun Int.baseOnRotation0(@DisplayCutout.BoundsPosition currentRotation: Int): Int {
     return when (currentRotation) {
         Surface.ROTATION_0 -> this
-        Surface.ROTATION_90 -> when (this) {
-            BOUNDS_POSITION_LEFT -> BOUNDS_POSITION_TOP
-            BOUNDS_POSITION_TOP -> BOUNDS_POSITION_RIGHT
-            BOUNDS_POSITION_RIGHT -> BOUNDS_POSITION_BOTTOM
-            else /* BOUNDS_POSITION_BOTTOM */ -> BOUNDS_POSITION_LEFT
-        }
-        Surface.ROTATION_270 -> when (this) {
-            BOUNDS_POSITION_LEFT -> BOUNDS_POSITION_BOTTOM
-            BOUNDS_POSITION_TOP -> BOUNDS_POSITION_LEFT
-            BOUNDS_POSITION_RIGHT -> BOUNDS_POSITION_TOP
-            else /* BOUNDS_POSITION_BOTTOM */ -> BOUNDS_POSITION_RIGHT
-        }
-        else /* Surface.ROTATION_180 */ -> when (this) {
-            BOUNDS_POSITION_LEFT -> BOUNDS_POSITION_RIGHT
-            BOUNDS_POSITION_TOP -> BOUNDS_POSITION_BOTTOM
-            BOUNDS_POSITION_RIGHT -> BOUNDS_POSITION_LEFT
-            else /* BOUNDS_POSITION_BOTTOM */ -> BOUNDS_POSITION_TOP
-        }
+        Surface.ROTATION_90 ->
+            when (this) {
+                BOUNDS_POSITION_LEFT -> BOUNDS_POSITION_TOP
+                BOUNDS_POSITION_TOP -> BOUNDS_POSITION_RIGHT
+                BOUNDS_POSITION_RIGHT -> BOUNDS_POSITION_BOTTOM
+                else /* BOUNDS_POSITION_BOTTOM */ -> BOUNDS_POSITION_LEFT
+            }
+        Surface.ROTATION_270 ->
+            when (this) {
+                BOUNDS_POSITION_LEFT -> BOUNDS_POSITION_BOTTOM
+                BOUNDS_POSITION_TOP -> BOUNDS_POSITION_LEFT
+                BOUNDS_POSITION_RIGHT -> BOUNDS_POSITION_TOP
+                else /* BOUNDS_POSITION_BOTTOM */ -> BOUNDS_POSITION_RIGHT
+            }
+        else /* Surface.ROTATION_180 */ ->
+            when (this) {
+                BOUNDS_POSITION_LEFT -> BOUNDS_POSITION_RIGHT
+                BOUNDS_POSITION_TOP -> BOUNDS_POSITION_BOTTOM
+                BOUNDS_POSITION_RIGHT -> BOUNDS_POSITION_LEFT
+                else /* BOUNDS_POSITION_BOTTOM */ -> BOUNDS_POSITION_TOP
+            }
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt
index 14ecc66..9aa7fd1 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt
@@ -23,19 +23,18 @@
 import android.view.Surface
 import android.view.View
 import android.view.ViewGroup
-import com.android.systemui.res.R
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
 import javax.inject.Inject
 
 /**
- * Provides privacy dot views for each orientation. The PrivacyDot orientation and visibility
- * of the privacy dot views are controlled by the PrivacyDotViewController.
+ * Provides privacy dot views for each orientation. The PrivacyDot orientation and visibility of the
+ * privacy dot views are controlled by the PrivacyDotViewController.
  */
 @SysUISingleton
-open class PrivacyDotDecorProviderFactory @Inject constructor(
-    @Main private val res: Resources
-) : DecorProviderFactory() {
+open class PrivacyDotDecorProviderFactory @Inject constructor(@Main private val res: Resources) :
+    DecorProviderFactory {
 
     private val isPrivacyDotEnabled: Boolean
         get() = res.getBoolean(R.bool.config_enablePrivacyDot)
@@ -51,22 +50,26 @@
                         viewId = R.id.privacy_dot_top_left_container,
                         alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
                         alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
-                        layoutId = R.layout.privacy_dot_top_left),
+                        layoutId = R.layout.privacy_dot_top_left,
+                    ),
                     PrivacyDotCornerDecorProviderImpl(
                         viewId = R.id.privacy_dot_top_right_container,
                         alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
                         alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
-                        layoutId = R.layout.privacy_dot_top_right),
+                        layoutId = R.layout.privacy_dot_top_right,
+                    ),
                     PrivacyDotCornerDecorProviderImpl(
                         viewId = R.id.privacy_dot_bottom_left_container,
                         alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
                         alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
-                        layoutId = R.layout.privacy_dot_bottom_left),
+                        layoutId = R.layout.privacy_dot_bottom_left,
+                    ),
                     PrivacyDotCornerDecorProviderImpl(
                         viewId = R.id.privacy_dot_bottom_right_container,
                         alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
                         alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
-                        layoutId = R.layout.privacy_dot_bottom_right)
+                        layoutId = R.layout.privacy_dot_bottom_right,
+                    ),
                 )
             } else {
                 emptyList()
@@ -78,7 +81,7 @@
     override val viewId: Int,
     @DisplayCutout.BoundsPosition override val alignedBound1: Int,
     @DisplayCutout.BoundsPosition override val alignedBound2: Int,
-    private val layoutId: Int
+    private val layoutId: Int,
 ) : CornerDecorProvider() {
 
     override fun onReloadResAndMeasure(
@@ -86,7 +89,7 @@
         reloadToken: Int,
         rotation: Int,
         tintColor: Int,
-        displayUniqueId: String?
+        displayUniqueId: String?,
     ) {
         // Do nothing here because it is handled inside PrivacyDotViewController
     }
@@ -95,7 +98,7 @@
         context: Context,
         parent: ViewGroup,
         @Surface.Rotation rotation: Int,
-        tintColor: Int
+        tintColor: Int,
     ): View {
         LayoutInflater.from(context).inflate(layoutId, parent, true)
         return parent.getChildAt(parent.childCount - 1 /* latest new added child */)
diff --git a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerDecorProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerDecorProviderFactory.kt
index 2f2c952f..39fd551 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerDecorProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerDecorProviderFactory.kt
@@ -21,65 +21,74 @@
 
 class RoundedCornerDecorProviderFactory(
     private val roundedCornerResDelegate: RoundedCornerResDelegate
-) : DecorProviderFactory() {
+) : DecorProviderFactory {
 
     override val hasProviders: Boolean
-        get() = roundedCornerResDelegate.run {
-            hasTop || hasBottom
-        }
+        get() = roundedCornerResDelegate.run { hasTop || hasBottom }
 
     override val providers: List<DecorProvider>
-    get() {
-        val hasTop = roundedCornerResDelegate.hasTop
-        val hasBottom = roundedCornerResDelegate.hasBottom
-        return when {
-            hasTop && hasBottom -> listOf(
-                RoundedCornerDecorProviderImpl(
-                    viewId = R.id.rounded_corner_top_left,
-                    alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
-                    alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
-                    roundedCornerResDelegate = roundedCornerResDelegate),
-                RoundedCornerDecorProviderImpl(
-                    viewId = R.id.rounded_corner_top_right,
-                    alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
-                    alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
-                    roundedCornerResDelegate = roundedCornerResDelegate),
-                RoundedCornerDecorProviderImpl(
-                    viewId = R.id.rounded_corner_bottom_left,
-                    alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
-                    alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
-                    roundedCornerResDelegate = roundedCornerResDelegate),
-                RoundedCornerDecorProviderImpl(
-                    viewId = R.id.rounded_corner_bottom_right,
-                    alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
-                    alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
-                    roundedCornerResDelegate = roundedCornerResDelegate)
-            )
-            hasTop -> listOf(
-                RoundedCornerDecorProviderImpl(
-                    viewId = R.id.rounded_corner_top_left,
-                    alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
-                    alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
-                    roundedCornerResDelegate = roundedCornerResDelegate),
-                RoundedCornerDecorProviderImpl(
-                    viewId = R.id.rounded_corner_top_right,
-                    alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
-                    alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
-                    roundedCornerResDelegate = roundedCornerResDelegate)
-            )
-            hasBottom -> listOf(
-                RoundedCornerDecorProviderImpl(
-                    viewId = R.id.rounded_corner_bottom_left,
-                    alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
-                    alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
-                    roundedCornerResDelegate = roundedCornerResDelegate),
-                RoundedCornerDecorProviderImpl(
-                    viewId = R.id.rounded_corner_bottom_right,
-                    alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
-                    alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
-                    roundedCornerResDelegate = roundedCornerResDelegate)
-            )
-            else -> emptyList()
+        get() {
+            val hasTop = roundedCornerResDelegate.hasTop
+            val hasBottom = roundedCornerResDelegate.hasBottom
+            return when {
+                hasTop && hasBottom ->
+                    listOf(
+                        RoundedCornerDecorProviderImpl(
+                            viewId = R.id.rounded_corner_top_left,
+                            alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
+                            alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
+                            roundedCornerResDelegate = roundedCornerResDelegate,
+                        ),
+                        RoundedCornerDecorProviderImpl(
+                            viewId = R.id.rounded_corner_top_right,
+                            alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
+                            alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
+                            roundedCornerResDelegate = roundedCornerResDelegate,
+                        ),
+                        RoundedCornerDecorProviderImpl(
+                            viewId = R.id.rounded_corner_bottom_left,
+                            alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
+                            alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
+                            roundedCornerResDelegate = roundedCornerResDelegate,
+                        ),
+                        RoundedCornerDecorProviderImpl(
+                            viewId = R.id.rounded_corner_bottom_right,
+                            alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
+                            alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
+                            roundedCornerResDelegate = roundedCornerResDelegate,
+                        ),
+                    )
+                hasTop ->
+                    listOf(
+                        RoundedCornerDecorProviderImpl(
+                            viewId = R.id.rounded_corner_top_left,
+                            alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
+                            alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
+                            roundedCornerResDelegate = roundedCornerResDelegate,
+                        ),
+                        RoundedCornerDecorProviderImpl(
+                            viewId = R.id.rounded_corner_top_right,
+                            alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
+                            alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
+                            roundedCornerResDelegate = roundedCornerResDelegate,
+                        ),
+                    )
+                hasBottom ->
+                    listOf(
+                        RoundedCornerDecorProviderImpl(
+                            viewId = R.id.rounded_corner_bottom_left,
+                            alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
+                            alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
+                            roundedCornerResDelegate = roundedCornerResDelegate,
+                        ),
+                        RoundedCornerDecorProviderImpl(
+                            viewId = R.id.rounded_corner_bottom_right,
+                            alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
+                            alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
+                            roundedCornerResDelegate = roundedCornerResDelegate,
+                        ),
+                    )
+                else -> emptyList()
+            }
         }
-    }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt
index 906f600..7b3380a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.keyboard.shortcut
 
-import android.app.Activity
 import com.android.systemui.CoreStartable
 import com.android.systemui.Flags.keyboardShortcutHelperRewrite
 import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository
@@ -31,8 +30,7 @@
 import com.android.systemui.keyboard.shortcut.qualifiers.InputShortcuts
 import com.android.systemui.keyboard.shortcut.qualifiers.MultitaskingShortcuts
 import com.android.systemui.keyboard.shortcut.qualifiers.SystemShortcuts
-import com.android.systemui.keyboard.shortcut.ui.ShortcutHelperActivityStarter
-import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity
+import com.android.systemui.keyboard.shortcut.ui.ShortcutHelperDialogStarter
 import dagger.Binds
 import dagger.Lazy
 import dagger.Module
@@ -44,11 +42,6 @@
 interface ShortcutHelperModule {
 
     @Binds
-    @IntoMap
-    @ClassKey(ShortcutHelperActivity::class)
-    fun activity(impl: ShortcutHelperActivity): Activity
-
-    @Binds
     @SystemShortcuts
     fun systemShortcutsSource(impl: SystemShortcutsSource): KeyboardShortcutGroupsSource
 
@@ -73,8 +66,8 @@
     companion object {
         @Provides
         @IntoMap
-        @ClassKey(ShortcutHelperActivityStarter::class)
-        fun starter(implLazy: Lazy<ShortcutHelperActivityStarter>): CoreStartable {
+        @ClassKey(ShortcutHelperDialogStarter::class)
+        fun starter(implLazy: Lazy<ShortcutHelperDialogStarter>): CoreStartable {
             return if (keyboardShortcutHelperRewrite()) {
                 implLazy.get()
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt
deleted file mode 100644
index fbf52e7..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2024 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.keyboard.shortcut.ui
-
-import android.content.Context
-import android.content.Intent
-import com.android.systemui.CoreStartable
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity
-import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-
-@SysUISingleton
-class ShortcutHelperActivityStarter(
-    private val context: Context,
-    @Application private val applicationScope: CoroutineScope,
-    private val viewModel: ShortcutHelperViewModel,
-    private val startActivity: (Intent) -> Unit,
-) : CoreStartable {
-
-    @Inject
-    constructor(
-        context: Context,
-        @Application applicationScope: CoroutineScope,
-        viewModel: ShortcutHelperViewModel,
-    ) : this(
-        context,
-        applicationScope,
-        viewModel,
-        startActivity = { intent -> context.startActivity(intent) }
-    )
-
-    override fun start() {
-        applicationScope.launch {
-            viewModel.shouldShow.collect { shouldShow ->
-                if (shouldShow) {
-                    startShortcutHelperActivity()
-                }
-            }
-        }
-    }
-
-    private fun startShortcutHelperActivity() {
-        startActivity(
-            Intent(context, ShortcutHelperActivity::class.java)
-                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-        )
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt
new file mode 100644
index 0000000..d33ab2a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 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.keyboard.shortcut.ui
+
+import android.app.Dialog
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.os.UserHandle
+import android.provider.Settings
+import androidx.annotation.VisibleForTesting
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelper
+import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelperBottomSheet
+import com.android.systemui.keyboard.shortcut.ui.composable.getWidth
+import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.phone.SystemUIDialogFactory
+import com.android.systemui.statusbar.phone.createBottomSheet
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+
+@SysUISingleton
+class ShortcutHelperDialogStarter
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    private val viewModel: ShortcutHelperViewModel,
+    private val dialogFactory: SystemUIDialogFactory,
+    private val activityStarter: ActivityStarter,
+) : CoreStartable {
+
+    @VisibleForTesting var dialog: Dialog? = null
+
+    override fun start() {
+        viewModel.shouldShow
+            .map { shouldShow ->
+                if (shouldShow) {
+                    dialog = createShortcutHelperDialog().also { it.show() }
+                } else {
+                    dialog?.dismiss()
+                }
+            }
+            .launchIn(applicationScope)
+    }
+
+    private fun createShortcutHelperDialog(): Dialog {
+        return dialogFactory.createBottomSheet(
+            content = { dialog ->
+                val shortcutsUiState by viewModel.shortcutsUiState.collectAsStateWithLifecycle()
+                ShortcutHelper(
+                    modifier = Modifier.width(getWidth()),
+                    shortcutsUiState = shortcutsUiState,
+                    onKeyboardSettingsClicked = { onKeyboardSettingsClicked(dialog) },
+                    onSearchQueryChanged = { viewModel.onSearchQueryChanged(it) },
+                )
+                dialog.setOnDismissListener { viewModel.onViewClosed() }
+            },
+            maxWidth = ShortcutHelperBottomSheet.LargeScreenWidthLandscape
+        )
+    }
+
+    private fun onKeyboardSettingsClicked(dialog: Dialog) {
+        try {
+            activityStarter.startActivity(
+                Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS).addFlags(FLAG_ACTIVITY_NEW_TASK),
+                /* dismissShade= */ true,
+                /* animationController = */ null,
+                /* showOverLockscreenWhenLocked = */ false,
+                UserHandle.CURRENT,
+            )
+        } catch (e: ActivityNotFoundException) {
+            // From the Settings docs: In some cases, a matching Activity may not exist, so ensure
+            // you safeguard against this.
+            e.printStackTrace()
+            return
+        }
+        dialog.dismiss()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelperUtils.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelperUtils.kt
index 1f0d696..e295564 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelperUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelperUtils.kt
@@ -16,9 +16,13 @@
 
 package com.android.systemui.keyboard.shortcut.ui.composable
 
+import android.content.res.Configuration
 import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
 
 /**
@@ -29,3 +33,21 @@
 fun hasCompactWindowSize() =
     LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact ||
         LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Compact
+
+@Composable
+fun getWidth(): Dp {
+    return if (hasCompactWindowSize()) {
+        ShortcutHelperBottomSheet.DefaultWidth
+    } else
+        when (LocalConfiguration.current.orientation) {
+            Configuration.ORIENTATION_LANDSCAPE ->
+                ShortcutHelperBottomSheet.LargeScreenWidthLandscape
+            else -> ShortcutHelperBottomSheet.LargeScreenWidthPortrait
+        }
+}
+
+object ShortcutHelperBottomSheet {
+    val DefaultWidth = 412.dp
+    val LargeScreenWidthPortrait = 704.dp
+    val LargeScreenWidthLandscape = 864.dp
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
deleted file mode 100644
index 52263ce..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Copyright (C) 2024 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.keyboard.shortcut.ui.view
-
-import android.content.ActivityNotFoundException
-import android.content.Intent
-import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
-import android.content.res.Configuration
-import android.os.Bundle
-import android.provider.Settings
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ModalBottomSheet
-import androidx.compose.material3.Surface
-import androidx.compose.material3.rememberModalBottomSheetState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.input.key.key
-import androidx.compose.ui.input.key.onKeyEvent
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.contentDescription
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.flowWithLifecycle
-import androidx.lifecycle.lifecycleScope
-import com.android.compose.theme.PlatformTheme
-import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelper
-import com.android.systemui.keyboard.shortcut.ui.composable.hasCompactWindowSize
-import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
-import com.android.systemui.res.R
-import com.android.systemui.settings.UserTracker
-import javax.inject.Inject
-import kotlinx.coroutines.launch
-
-/**
- * Activity that hosts the new version of the keyboard shortcut helper. It will be used both for
- * small and large screen devices.
- */
-class ShortcutHelperActivity
-@Inject
-constructor(private val userTracker: UserTracker, private val viewModel: ShortcutHelperViewModel) :
-    ComponentActivity() {
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        setupEdgeToEdge()
-        super.onCreate(savedInstanceState)
-        setContent { Content() }
-        observeFinishRequired()
-        viewModel.onViewOpened()
-    }
-
-    @Composable
-    private fun Content() {
-        CompositionLocalProvider(LocalContext provides userTracker.userContext) {
-            PlatformTheme { BottomSheet { finish() } }
-        }
-    }
-
-    @OptIn(ExperimentalMaterial3Api::class)
-    @Composable
-    private fun BottomSheet(onDismiss: () -> Unit) {
-        ModalBottomSheet(
-            onDismissRequest = { onDismiss() },
-            modifier =
-                Modifier.width(getWidth()).padding(top = getTopPadding()).onKeyEvent {
-                    if (it.key == Key.Escape) {
-                        onDismiss()
-                        true
-                    } else false
-                },
-            sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
-            dragHandle = { DragHandle() },
-        ) {
-            val shortcutsUiState by viewModel.shortcutsUiState.collectAsStateWithLifecycle()
-            ShortcutHelper(
-                shortcutsUiState = shortcutsUiState,
-                onKeyboardSettingsClicked = ::onKeyboardSettingsClicked,
-                onSearchQueryChanged = { viewModel.onSearchQueryChanged(it) },
-            )
-        }
-    }
-
-    @Composable
-    fun DragHandle() {
-        val dragHandleContentDescription =
-            stringResource(id = R.string.shortcut_helper_content_description_drag_handle)
-        Surface(
-            modifier =
-                Modifier.padding(top = 16.dp, bottom = 6.dp).semantics {
-                    contentDescription = dragHandleContentDescription
-                },
-            color = MaterialTheme.colorScheme.outlineVariant,
-            shape = MaterialTheme.shapes.extraLarge,
-        ) {
-            Box(Modifier.size(width = 32.dp, height = 4.dp))
-        }
-    }
-
-    private fun onKeyboardSettingsClicked() {
-        try {
-            startActivityAsUser(
-                Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS).addFlags(FLAG_ACTIVITY_NEW_TASK),
-                userTracker.userHandle,
-            )
-        } catch (e: ActivityNotFoundException) {
-            // From the Settings docs: In some cases, a matching Activity may not exist, so ensure
-            // you safeguard against this.
-            e.printStackTrace()
-        }
-    }
-
-    override fun onDestroy() {
-        super.onDestroy()
-        if (isFinishing) {
-            viewModel.onViewClosed()
-        }
-    }
-
-    private fun observeFinishRequired() {
-        lifecycleScope.launch {
-            viewModel.shouldShow.flowWithLifecycle(lifecycle).collect { shouldShow ->
-                if (!shouldShow) {
-                    finish()
-                }
-            }
-        }
-    }
-
-    private fun setupEdgeToEdge() {
-        // Draw behind system bars
-        window.setDecorFitsSystemWindows(false)
-    }
-
-    @Composable
-    private fun getTopPadding(): Dp {
-        return if (hasCompactWindowSize()) DefaultTopPadding else LargeScreenTopPadding
-    }
-
-    @Composable
-    private fun getWidth(): Dp {
-        return if (hasCompactWindowSize()) {
-            DefaultWidth
-        } else
-            when (LocalConfiguration.current.orientation) {
-                Configuration.ORIENTATION_LANDSCAPE -> LargeScreenWidthLandscape
-                else -> LargeScreenWidthPortrait
-            }
-    }
-
-    companion object {
-        private val DefaultTopPadding = 64.dp
-        private val LargeScreenTopPadding = 72.dp
-        private val DefaultWidth = 412.dp
-        private val LargeScreenWidthPortrait = 704.dp
-        private val LargeScreenWidthLandscape = 864.dp
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index 2961d05..742f435 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -170,12 +170,11 @@
                 // Set different layout for each device
                 if (device.isMutingExpectedDevice()
                         && !mController.isCurrentConnectedDeviceRemote()) {
-                    updateTitleIcon(R.drawable.media_output_icon_volume,
-                            mController.getColorItemContent());
+                    updateUnmutedVolumeIcon(device);
                     mCurrentActivePosition = position;
                     updateFullItemClickListener(v -> onItemClick(v, device));
                     setSingleLineLayout(getItemTitle(device));
-                    initFakeActiveDevice();
+                    initFakeActiveDevice(device);
                 } else if (device.hasSubtext()) {
                     boolean isActiveWithOngoingSession =
                             (device.hasOngoingSession() && (currentlyConnected || isDeviceIncluded(
@@ -184,8 +183,7 @@
                             && isActiveWithOngoingSession;
                     if (isActiveWithOngoingSession) {
                         mCurrentActivePosition = position;
-                        updateTitleIcon(R.drawable.media_output_icon_volume,
-                                mController.getColorItemContent());
+                        updateUnmutedVolumeIcon(device);
                         mSubTitleText.setText(device.getSubtextString());
                         updateTwoLineLayoutContentAlpha(DEVICE_CONNECTED_ALPHA);
                         updateEndClickAreaAsSessionEditing(device,
@@ -199,9 +197,7 @@
                     } else {
                         if (currentlyConnected) {
                             mCurrentActivePosition = position;
-                            updateTitleIcon(R.drawable.media_output_icon_volume,
-                                    mController.getColorItemContent());
-                            initSeekbar(device, isCurrentSeekbarInvisible);
+                            updateUnmutedVolumeIcon(device);
                         } else {
                             setUpDeviceIcon(device);
                         }
@@ -243,8 +239,7 @@
                     // selected device in group
                     boolean isDeviceDeselectable = isDeviceIncluded(
                             mController.getDeselectableMediaDevice(), device);
-                    updateTitleIcon(R.drawable.media_output_icon_volume,
-                            mController.getColorItemContent());
+                    updateUnmutedVolumeIcon(device);
                     updateGroupableCheckBox(true, isDeviceDeselectable, device);
                     updateEndClickArea(device, isDeviceDeselectable);
                     disableFocusPropertyForView(mContainerLayout);
@@ -264,8 +259,7 @@
                         setSingleLineLayout(getItemTitle(device));
                     } else if (device.hasOngoingSession()) {
                         mCurrentActivePosition = position;
-                        updateTitleIcon(R.drawable.media_output_icon_volume,
-                                mController.getColorItemContent());
+                        updateUnmutedVolumeIcon(device);
                         updateEndClickAreaAsSessionEditing(device, device.isHostForOngoingSession()
                                 ? R.drawable.media_output_status_edit_session
                                 : R.drawable.ic_sound_bars_anim);
@@ -278,8 +272,7 @@
                             && !mController.getSelectableMediaDevice().isEmpty()) {
                         //If device is connected and there's other selectable devices, layout as
                         // one of selected devices.
-                        updateTitleIcon(R.drawable.media_output_icon_volume,
-                                mController.getColorItemContent());
+                        updateUnmutedVolumeIcon(device);
                         boolean isDeviceDeselectable = isDeviceIncluded(
                                 mController.getDeselectableMediaDevice(), device);
                         updateGroupableCheckBox(true, isDeviceDeselectable, device);
@@ -291,8 +284,7 @@
                                 true /* showEndTouchArea */);
                         initSeekbar(device, isCurrentSeekbarInvisible);
                     } else {
-                        updateTitleIcon(R.drawable.media_output_icon_volume,
-                                mController.getColorItemContent());
+                        updateUnmutedVolumeIcon(device);
                         disableFocusPropertyForView(mContainerLayout);
                         setUpContentDescriptionForView(mSeekBar, device);
                         mCurrentActivePosition = position;
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index 63a7e01..574ccee 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -47,6 +47,7 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.android.settingslib.media.InputMediaDevice;
 import com.android.settingslib.media.MediaDevice;
 import com.android.settingslib.utils.ThreadUtils;
 import com.android.systemui.res.R;
@@ -321,18 +322,20 @@
                     // Check if response volume match with the latest request, to ignore obsolete
                     // response
                     if (isCurrentSeekbarInvisible && !mIsInitVolumeFirstTime) {
-                        updateTitleIcon(currentVolume == 0 ? R.drawable.media_output_icon_volume_off
-                                        : R.drawable.media_output_icon_volume,
-                                mController.getColorItemContent());
+                        if (currentVolume == 0) {
+                            updateMutedVolumeIcon(device);
+                        } else {
+                            updateUnmutedVolumeIcon(device);
+                        }
                     } else {
                         if (!mVolumeAnimator.isStarted()) {
                             int percentage =
                                     (int) ((double) currentVolume * VOLUME_PERCENTAGE_SCALE_SIZE
                                             / (double) mSeekBar.getMax());
                             if (percentage == 0) {
-                                updateMutedVolumeIcon();
+                                updateMutedVolumeIcon(device);
                             } else {
-                                updateUnmutedVolumeIcon();
+                                updateUnmutedVolumeIcon(device);
                             }
                             mSeekBar.setVolume(currentVolume);
                             mLatestUpdateVolume = -1;
@@ -340,7 +343,7 @@
                     }
                 } else if (currentVolume == 0) {
                     mSeekBar.resetVolume();
-                    updateMutedVolumeIcon();
+                    updateMutedVolumeIcon(device);
                 }
                 if (currentVolume == mLatestUpdateVolume) {
                     mLatestUpdateVolume = -1;
@@ -365,7 +368,7 @@
                             R.string.media_output_dialog_volume_percentage, percentage));
                     mVolumeValueText.setVisibility(View.VISIBLE);
                     if (mStartFromMute) {
-                        updateUnmutedVolumeIcon();
+                        updateUnmutedVolumeIcon(device);
                         mStartFromMute = false;
                     }
                     if (progressToVolume != deviceVolume) {
@@ -390,9 +393,9 @@
                             seekBar.getProgress());
                     if (currentVolume == 0) {
                         seekBar.setProgress(0);
-                        updateMutedVolumeIcon();
+                        updateMutedVolumeIcon(device);
                     } else {
-                        updateUnmutedVolumeIcon();
+                        updateUnmutedVolumeIcon(device);
                     }
                     mTitleIcon.setVisibility(View.VISIBLE);
                     mVolumeValueText.setVisibility(View.GONE);
@@ -402,36 +405,48 @@
             });
         }
 
-        void updateMutedVolumeIcon() {
+        void updateMutedVolumeIcon(MediaDevice device) {
             mIconAreaLayout.setBackground(
                     mContext.getDrawable(R.drawable.media_output_item_background_active));
-            updateTitleIcon(R.drawable.media_output_icon_volume_off,
-                    mController.getColorItemContent());
+            updateTitleIcon(device, true /* isMutedVolumeIcon */);
         }
 
-        void updateUnmutedVolumeIcon() {
+        void updateUnmutedVolumeIcon(MediaDevice device) {
             mIconAreaLayout.setBackground(
                     mContext.getDrawable(R.drawable.media_output_title_icon_area)
             );
-            updateTitleIcon(R.drawable.media_output_icon_volume,
-                    mController.getColorItemContent());
+            updateTitleIcon(device, false /* isMutedVolumeIcon */);
         }
 
-        void updateTitleIcon(@DrawableRes int id, int color) {
+        void updateTitleIcon(MediaDevice device, boolean isMutedVolumeIcon) {
+            boolean isInputMediaDevice = device instanceof InputMediaDevice;
+            int id = getDrawableId(isInputMediaDevice, isMutedVolumeIcon);
             mTitleIcon.setImageDrawable(mContext.getDrawable(id));
-            mTitleIcon.setImageTintList(ColorStateList.valueOf(color));
+            mTitleIcon.setImageTintList(ColorStateList.valueOf(mController.getColorItemContent()));
             mIconAreaLayout.setBackgroundTintList(
                     ColorStateList.valueOf(mController.getColorSeekbarProgress()));
         }
 
+        @VisibleForTesting
+        int getDrawableId(boolean isInputDevice, boolean isMutedVolumeIcon) {
+            // Returns the microphone icon when the flag is enabled and the device is an input
+            // device.
+            if (com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl()
+                    && isInputDevice) {
+                return isMutedVolumeIcon ? R.drawable.ic_mic_off : R.drawable.ic_mic_26dp;
+            }
+            return isMutedVolumeIcon
+                    ? R.drawable.media_output_icon_volume_off
+                    : R.drawable.media_output_icon_volume;
+        }
+
         void updateIconAreaClickListener(View.OnClickListener listener) {
             mIconAreaLayout.setOnClickListener(listener);
         }
 
-        void initFakeActiveDevice() {
+        void initFakeActiveDevice(MediaDevice device) {
             disableSeekBar();
-            updateTitleIcon(R.drawable.media_output_icon_volume,
-                    mController.getColorItemContent());
+            updateTitleIcon(device, false /* isMutedIcon */);
             final Drawable backgroundDrawable = mContext.getDrawable(
                                     R.drawable.media_output_item_background_active)
                             .mutate();
@@ -518,13 +533,13 @@
                     mController.logInteractionUnmuteDevice(device);
                     mSeekBar.setVolume(UNMUTE_DEFAULT_VOLUME);
                     mController.adjustVolume(device, UNMUTE_DEFAULT_VOLUME);
-                    updateUnmutedVolumeIcon();
+                    updateUnmutedVolumeIcon(device);
                     mIconAreaLayout.setOnTouchListener(((iconV, event) -> false));
                 } else {
                     mController.logInteractionMuteDevice(device);
                     mSeekBar.resetVolume();
                     mController.adjustVolume(device, 0);
-                    updateMutedVolumeIcon();
+                    updateMutedVolumeIcon(device);
                     mIconAreaLayout.setOnTouchListener(((iconV, event) -> {
                         mSeekBar.dispatchTouchEvent(event);
                         return false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
index 8d3f728..30f564f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar;
 
+import android.app.Flags;
 import android.app.Notification;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
@@ -60,20 +61,6 @@
             return row.getEntry().getSbn().getNotification();
         }
     };
-    private static final IconComparator ICON_VISIBILITY_COMPARATOR = new IconComparator() {
-        public boolean compare(View parent, View child, Object parentData,
-                Object childData) {
-            return hasSameIcon(parentData, childData)
-                    && hasSameColor(parentData, childData);
-        }
-    };
-    private static final IconComparator GREY_COMPARATOR = new IconComparator() {
-        public boolean compare(View parent, View child, Object parentData,
-                Object childData) {
-            return !hasSameIcon(parentData, childData)
-                    || hasSameColor(parentData, childData);
-        }
-    };
     private static final ResultApplicator GREY_APPLICATOR = new ResultApplicator() {
         @Override
         public void apply(View parent, View view, boolean apply, boolean reset) {
@@ -90,34 +77,58 @@
 
     public NotificationGroupingUtil(ExpandableNotificationRow row) {
         mRow = row;
+
+        final IconComparator iconVisibilityComparator = new IconComparator(mRow) {
+            public boolean compare(View parent, View child, Object parentData,
+                    Object childData) {
+                return hasSameIcon(parentData, childData)
+                        && hasSameColor(parentData, childData);
+            }
+        };
+        final IconComparator greyComparator = new IconComparator(mRow) {
+            public boolean compare(View parent, View child, Object parentData,
+                    Object childData) {
+                if (Flags.notificationsRedesignAppIcons() && mRow.isShowingAppIcon()) {
+                    return false;
+                }
+                return !hasSameIcon(parentData, childData)
+                        || hasSameColor(parentData, childData);
+            }
+        };
+
         // To hide the icons if they are the same and the color is the same
         mProcessors.add(new Processor(mRow,
                 com.android.internal.R.id.icon,
                 ICON_EXTRACTOR,
-                ICON_VISIBILITY_COMPARATOR,
+                iconVisibilityComparator,
                 VISIBILITY_APPLICATOR));
-        // To grey them out the icons and expand button when the icons are not the same
+        // To grey out the icons when they are not the same, or they have the same color
         mProcessors.add(new Processor(mRow,
                 com.android.internal.R.id.status_bar_latest_event_content,
                 ICON_EXTRACTOR,
-                GREY_COMPARATOR,
+                greyComparator,
                 GREY_APPLICATOR));
+        // To show the large icon on the left side instead if all the small icons are the same
         mProcessors.add(new Processor(mRow,
                 com.android.internal.R.id.status_bar_latest_event_content,
                 ICON_EXTRACTOR,
-                ICON_VISIBILITY_COMPARATOR,
+                iconVisibilityComparator,
                 LEFT_ICON_APPLICATOR));
+        // To only show the work profile icon in the group header
         mProcessors.add(new Processor(mRow,
                 com.android.internal.R.id.profile_badge,
                 null /* Extractor */,
                 BADGE_COMPARATOR,
                 VISIBILITY_APPLICATOR));
+        // To hide the app name in group children
         mProcessors.add(new Processor(mRow,
                 com.android.internal.R.id.app_name_text,
                 null,
                 APP_NAME_COMPARATOR,
                 APP_NAME_APPLICATOR));
+        // To hide the header text if it's the same
         mProcessors.add(Processor.forTextView(mRow, com.android.internal.R.id.header_text));
+
         mDividers.add(com.android.internal.R.id.header_text_divider);
         mDividers.add(com.android.internal.R.id.header_text_secondary_divider);
         mDividers.add(com.android.internal.R.id.time_divider);
@@ -261,6 +272,7 @@
             mParentData = mExtractor == null ? null : mExtractor.extractData(mParentRow);
             mApply = !mComparator.isEmpty(mParentView);
         }
+
         public void compareToGroupParent(ExpandableNotificationRow row) {
             if (!mApply) {
                 return;
@@ -356,12 +368,21 @@
     }
 
     private abstract static class IconComparator implements ViewComparator {
+        private final ExpandableNotificationRow mRow;
+
+        IconComparator(ExpandableNotificationRow row) {
+            mRow = row;
+        }
+
         @Override
         public boolean compare(View parent, View child, Object parentData, Object childData) {
             return false;
         }
 
         protected boolean hasSameIcon(Object parentData, Object childData) {
+            if (Flags.notificationsRedesignAppIcons() && mRow.isShowingAppIcon()) {
+                return true;
+            }
             Icon parentIcon = getIcon((Notification) parentData);
             Icon childIcon = getIcon((Notification) childData);
             return parentIcon.sameAs(childIcon);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index f6f4503..f65ae67 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -17,8 +17,10 @@
 package com.android.systemui.statusbar.dagger
 
 import android.content.Context
+import com.android.systemui.CameraProtectionLoader
 import com.android.systemui.CoreStartable
 import com.android.systemui.SysUICutoutProvider
+import com.android.systemui.SysUICutoutProviderImpl
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogBufferFactory
@@ -114,6 +116,16 @@
 
         @Provides
         @SysUISingleton
+        fun sysUiCutoutProvider(
+            factory: SysUICutoutProviderImpl.Factory,
+            context: Context,
+            cameraProtectionLoader: CameraProtectionLoader,
+        ): SysUICutoutProvider {
+            return factory.create(context, cameraProtectionLoader)
+        }
+
+        @Provides
+        @SysUISingleton
         fun contentInsetsProvider(
             factory: StatusBarContentInsetsProviderImpl.Factory,
             context: Context,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt
index ed96482..415d990 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt
@@ -23,7 +23,7 @@
 import dagger.Module
 import dagger.Provides
 
-@Module
+@Module(includes = [SystemEventChipAnimationControllerModule::class])
 interface StatusBarEventsModule {
 
     companion object {
@@ -41,4 +41,4 @@
     fun bindSystemStatusAnimationScheduler(
         systemStatusAnimationSchedulerImpl: SystemStatusAnimationSchedulerImpl
     ): SystemStatusAnimationScheduler
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
index bf7e879..35816c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
@@ -31,22 +31,46 @@
 import androidx.core.animation.AnimatorSet
 import androidx.core.animation.ValueAnimator
 import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.window.StatusBarWindowController
 import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
 import com.android.systemui.util.animation.AnimationUtil.Companion.frames
-import javax.inject.Inject
+import dagger.Module
+import dagger.Provides
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 import kotlin.math.roundToInt
 
-/**
- * Controls the view for system event animations.
- */
-class SystemEventChipAnimationController @Inject constructor(
-    private val context: Context,
-    private val statusBarWindowControllerStore: StatusBarWindowControllerStore,
-    private val contentInsetsProvider: StatusBarContentInsetsProvider,
-) : SystemStatusAnimationCallback {
+/** Controls the view for system event animations. */
+interface SystemEventChipAnimationController : SystemStatusAnimationCallback {
+
+    /**
+     * Give the chip controller a chance to inflate and configure the chip view before we start
+     * animating
+     */
+    fun prepareChipAnimation(viewCreator: ViewCreator)
+
+    fun init()
+
+    /** Announces [contentDescriptions] for accessibility. */
+    fun announceForAccessibility(contentDescriptions: String)
+
+    override fun onSystemEventAnimationBegin(): Animator
+
+    override fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator
+}
+
+class SystemEventChipAnimationControllerImpl
+@AssistedInject
+constructor(
+    @Assisted private val context: Context,
+    @Assisted private val statusBarWindowController: StatusBarWindowController,
+    @Assisted private val contentInsetsProvider: StatusBarContentInsetsProvider,
+) : SystemEventChipAnimationController {
 
     private lateinit var animationWindowView: FrameLayout
     private lateinit var themedContext: ContextThemeWrapper
@@ -57,25 +81,27 @@
     private var animationDirection = LEFT
 
     @VisibleForTesting var chipBounds = Rect()
-    private val chipWidth get() = chipBounds.width()
-    private val chipRight get() = chipBounds.right
-    private val chipLeft get() = chipBounds.left
-    private var chipMinWidth = context.resources.getDimensionPixelSize(
-            R.dimen.ongoing_appops_chip_min_animation_width)
+    private val chipWidth
+        get() = chipBounds.width()
 
-    private val dotSize = context.resources.getDimensionPixelSize(
-            R.dimen.ongoing_appops_dot_diameter)
+    private val chipRight
+        get() = chipBounds.right
+
+    private val chipLeft
+        get() = chipBounds.left
+
+    private var chipMinWidth =
+        context.resources.getDimensionPixelSize(R.dimen.ongoing_appops_chip_min_animation_width)
+
+    private val dotSize =
+        context.resources.getDimensionPixelSize(R.dimen.ongoing_appops_dot_diameter)
     // Use during animation so that multiple animators can update the drawing rect
     private var animRect = Rect()
 
     // TODO: move to dagger
     @VisibleForTesting var initialized = false
 
-    /**
-     * Give the chip controller a chance to inflate and configure the chip view before we start
-     * animating
-     */
-    fun prepareChipAnimation(viewCreator: ViewCreator) {
+    override fun prepareChipAnimation(viewCreator: ViewCreator) {
         if (!initialized) {
             init()
         }
@@ -83,47 +109,62 @@
 
         // Initialize the animated view
         val insets = contentInsetsProvider.getStatusBarContentInsetsForCurrentRotation()
-        currentAnimatedView = viewCreator(themedContext).also {
-            animationWindowView.addView(
+        currentAnimatedView =
+            viewCreator(themedContext).also {
+                animationWindowView.addView(
                     it.view,
                     layoutParamsDefault(
-                            if (animationWindowView.isLayoutRtl) insets.left
-                            else insets.right))
-            it.view.alpha = 0f
-            // For some reason, the window view's measured width is always 0 here, so use the
-            // parent (status bar)
-            it.view.measure(
+                        if (animationWindowView.isLayoutRtl) insets.left else insets.right
+                    ),
+                )
+                it.view.alpha = 0f
+                // For some reason, the window view's measured width is always 0 here, so use the
+                // parent (status bar)
+                it.view.measure(
                     View.MeasureSpec.makeMeasureSpec(
-                            (animationWindowView.parent as View).width, AT_MOST),
+                        (animationWindowView.parent as View).width,
+                        AT_MOST,
+                    ),
                     View.MeasureSpec.makeMeasureSpec(
-                            (animationWindowView.parent as View).height, AT_MOST))
+                        (animationWindowView.parent as View).height,
+                        AT_MOST,
+                    ),
+                )
 
-            updateChipBounds(it, contentInsetsProvider.getStatusBarContentAreaForCurrentRotation())
-        }
+                updateChipBounds(
+                    it,
+                    contentInsetsProvider.getStatusBarContentAreaForCurrentRotation(),
+                )
+            }
     }
 
     override fun onSystemEventAnimationBegin(): Animator {
         initializeAnimRect()
 
-        val alphaIn = ValueAnimator.ofFloat(0f, 1f).apply {
-            startDelay = 7.frames
-            duration = 5.frames
-            interpolator = null
-            addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float }
-        }
+        val alphaIn =
+            ValueAnimator.ofFloat(0f, 1f).apply {
+                startDelay = 7.frames
+                duration = 5.frames
+                interpolator = null
+                addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float }
+            }
         currentAnimatedView?.contentView?.alpha = 0f
-        val contentAlphaIn = ValueAnimator.ofFloat(0f, 1f).apply {
-            startDelay = 10.frames
-            duration = 10.frames
-            interpolator = null
-            addUpdateListener { currentAnimatedView?.contentView?.alpha = animatedValue as Float }
-        }
-        val moveIn = ValueAnimator.ofInt(chipMinWidth, chipWidth).apply {
-            startDelay = 7.frames
-            duration = 23.frames
-            interpolator = STATUS_BAR_X_MOVE_IN
-            addUpdateListener { updateAnimatedViewBoundsWidth(animatedValue as Int) }
-        }
+        val contentAlphaIn =
+            ValueAnimator.ofFloat(0f, 1f).apply {
+                startDelay = 10.frames
+                duration = 10.frames
+                interpolator = null
+                addUpdateListener {
+                    currentAnimatedView?.contentView?.alpha = animatedValue as Float
+                }
+            }
+        val moveIn =
+            ValueAnimator.ofInt(chipMinWidth, chipWidth).apply {
+                startDelay = 7.frames
+                duration = 23.frames
+                interpolator = STATUS_BAR_X_MOVE_IN
+                addUpdateListener { updateAnimatedViewBoundsWidth(animatedValue as Int) }
+            }
         val animSet = AnimatorSet()
         animSet.playTogether(alphaIn, contentAlphaIn, moveIn)
         return animSet
@@ -131,75 +172,80 @@
 
     override fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator {
         initializeAnimRect()
-        val finish = if (hasPersistentDot) {
-            createMoveOutAnimationForDot()
-        } else {
-            createMoveOutAnimationDefault()
-        }
-
-        finish.addListener(object : AnimatorListenerAdapter() {
-            override fun onAnimationEnd(animation: Animator) {
-                animationWindowView.removeView(currentAnimatedView!!.view)
+        val finish =
+            if (hasPersistentDot) {
+                createMoveOutAnimationForDot()
+            } else {
+                createMoveOutAnimationDefault()
             }
-        })
+
+        finish.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    animationWindowView.removeView(currentAnimatedView!!.view)
+                }
+            }
+        )
 
         return finish
     }
 
     private fun createMoveOutAnimationForDot(): Animator {
-        val width1 = ValueAnimator.ofInt(chipWidth, chipMinWidth).apply {
-            duration = 9.frames
-            interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_1
-            addUpdateListener {
-                updateAnimatedViewBoundsWidth(animatedValue as Int)
+        val width1 =
+            ValueAnimator.ofInt(chipWidth, chipMinWidth).apply {
+                duration = 9.frames
+                interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_1
+                addUpdateListener { updateAnimatedViewBoundsWidth(animatedValue as Int) }
             }
-        }
 
-        val width2 = ValueAnimator.ofInt(chipMinWidth, dotSize).apply {
-            startDelay = 9.frames
-            duration = 20.frames
-            interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_2
-            addUpdateListener {
-                updateAnimatedViewBoundsWidth(animatedValue as Int)
+        val width2 =
+            ValueAnimator.ofInt(chipMinWidth, dotSize).apply {
+                startDelay = 9.frames
+                duration = 20.frames
+                interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_2
+                addUpdateListener { updateAnimatedViewBoundsWidth(animatedValue as Int) }
             }
-        }
 
         val keyFrame1Height = dotSize * 2
         val chipVerticalCenter = chipBounds.top + chipBounds.height() / 2
-        val height1 = ValueAnimator.ofInt(chipBounds.height(), keyFrame1Height).apply {
-            startDelay = 8.frames
-            duration = 6.frames
-            interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_1
-            addUpdateListener {
-                updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter)
+        val height1 =
+            ValueAnimator.ofInt(chipBounds.height(), keyFrame1Height).apply {
+                startDelay = 8.frames
+                duration = 6.frames
+                interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_1
+                addUpdateListener {
+                    updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter)
+                }
             }
-        }
 
-        val height2 = ValueAnimator.ofInt(keyFrame1Height, dotSize).apply {
-            startDelay = 14.frames
-            duration = 15.frames
-            interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_2
-            addUpdateListener {
-                updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter)
+        val height2 =
+            ValueAnimator.ofInt(keyFrame1Height, dotSize).apply {
+                startDelay = 14.frames
+                duration = 15.frames
+                interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_2
+                addUpdateListener {
+                    updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter)
+                }
             }
-        }
 
         // Move the chip view to overlap exactly with the privacy dot. The chip displays by default
         // exactly adjacent to the dot, so we can just move over by the diameter of the dot itself
-        val moveOut = ValueAnimator.ofInt(0, dotSize).apply {
-            startDelay = 3.frames
-            duration = 11.frames
-            interpolator = STATUS_CHIP_MOVE_TO_DOT
-            addUpdateListener {
-                // If RTL, we can just invert the move
-                val amt = if (animationDirection == LEFT) {
-                        animatedValue as Int
-                } else {
-                    -(animatedValue as Int)
+        val moveOut =
+            ValueAnimator.ofInt(0, dotSize).apply {
+                startDelay = 3.frames
+                duration = 11.frames
+                interpolator = STATUS_CHIP_MOVE_TO_DOT
+                addUpdateListener {
+                    // If RTL, we can just invert the move
+                    val amt =
+                        if (animationDirection == LEFT) {
+                            animatedValue as Int
+                        } else {
+                            -(animatedValue as Int)
+                        }
+                    updateAnimatedBoundsX(amt)
                 }
-                updateAnimatedBoundsX(amt)
             }
-        }
 
         val animSet = AnimatorSet()
         animSet.playTogether(width1, width2, height1, height2, moveOut)
@@ -207,71 +253,80 @@
     }
 
     private fun createMoveOutAnimationDefault(): Animator {
-        val alphaOut = ValueAnimator.ofFloat(1f, 0f).apply {
-            startDelay = 6.frames
-            duration = 6.frames
-            interpolator = null
-            addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float }
-        }
+        val alphaOut =
+            ValueAnimator.ofFloat(1f, 0f).apply {
+                startDelay = 6.frames
+                duration = 6.frames
+                interpolator = null
+                addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float }
+            }
 
-        val contentAlphaOut = ValueAnimator.ofFloat(1f, 0f).apply {
-            duration = 5.frames
-            interpolator = null
-            addUpdateListener { currentAnimatedView?.contentView?.alpha = animatedValue as Float }
-        }
-
-        val moveOut = ValueAnimator.ofInt(chipWidth, chipMinWidth).apply {
-            duration = 23.frames
-            interpolator = STATUS_BAR_X_MOVE_OUT
-            addUpdateListener {
-                currentAnimatedView?.apply {
-                    updateAnimatedViewBoundsWidth(animatedValue as Int)
+        val contentAlphaOut =
+            ValueAnimator.ofFloat(1f, 0f).apply {
+                duration = 5.frames
+                interpolator = null
+                addUpdateListener {
+                    currentAnimatedView?.contentView?.alpha = animatedValue as Float
                 }
             }
-        }
+
+        val moveOut =
+            ValueAnimator.ofInt(chipWidth, chipMinWidth).apply {
+                duration = 23.frames
+                interpolator = STATUS_BAR_X_MOVE_OUT
+                addUpdateListener {
+                    currentAnimatedView?.apply {
+                        updateAnimatedViewBoundsWidth(animatedValue as Int)
+                    }
+                }
+            }
 
         val animSet = AnimatorSet()
         animSet.playTogether(alphaOut, contentAlphaOut, moveOut)
         return animSet
     }
 
-    fun init() {
+    override fun init() {
         initialized = true
         themedContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
-        animationWindowView = LayoutInflater.from(themedContext)
-                .inflate(R.layout.system_event_animation_window, null) as FrameLayout
+        animationWindowView =
+            LayoutInflater.from(themedContext).inflate(R.layout.system_event_animation_window, null)
+                as FrameLayout
         // Matches status_bar.xml
         val height = themedContext.resources.getDimensionPixelSize(R.dimen.status_bar_height)
         val lp = FrameLayout.LayoutParams(MATCH_PARENT, height)
         lp.gravity = Gravity.END or Gravity.TOP
-        statusBarWindowControllerStore.defaultDisplay.addViewToWindow(animationWindowView, lp)
+        statusBarWindowController.addViewToWindow(animationWindowView, lp)
         animationWindowView.clipToPadding = false
         animationWindowView.clipChildren = false
 
         // Use contentInsetsProvider rather than configuration controller, since we only care
         // about status bar dimens
-        contentInsetsProvider.addCallback(object : StatusBarContentInsetsChangedListener {
-            override fun onStatusBarContentInsetsChanged() {
-                val newContentArea = contentInsetsProvider
-                    .getStatusBarContentAreaForCurrentRotation()
-                updateDimens(newContentArea)
+        contentInsetsProvider.addCallback(
+            object : StatusBarContentInsetsChangedListener {
+                override fun onStatusBarContentInsetsChanged() {
+                    val newContentArea =
+                        contentInsetsProvider.getStatusBarContentAreaForCurrentRotation()
+                    updateDimens(newContentArea)
 
-                // If we are currently animating, we have to re-solve for the chip bounds. If we're
-                // not animating then [prepareChipAnimation] will take care of it for us
-                currentAnimatedView?.let {
-                    updateChipBounds(it, newContentArea)
-                    // Since updateCurrentAnimatedView can only be called during an animation, we
-                    // have to create a dummy animator here to apply the new chip bounds
-                    val animator = ValueAnimator.ofInt(0, 1).setDuration(0)
-                    animator.addUpdateListener { updateCurrentAnimatedView() }
-                    animator.start()
+                    // If we are currently animating, we have to re-solve for the chip bounds. If
+                    // we're
+                    // not animating then [prepareChipAnimation] will take care of it for us
+                    currentAnimatedView?.let {
+                        updateChipBounds(it, newContentArea)
+                        // Since updateCurrentAnimatedView can only be called during an animation,
+                        // we
+                        // have to create a dummy animator here to apply the new chip bounds
+                        val animator = ValueAnimator.ofInt(0, 1).setDuration(0)
+                        animator.addUpdateListener { updateCurrentAnimatedView() }
+                        animator.start()
+                    }
                 }
             }
-        })
+        )
     }
 
-    /** Announces [contentDescriptions] for accessibility. */
-    fun announceForAccessibility(contentDescriptions: String) {
+    override fun announceForAccessibility(contentDescriptions: String) {
         currentAnimatedView?.view?.announceForAccessibility(contentDescriptions)
     }
 
@@ -283,9 +338,9 @@
     }
 
     /**
-     * Use the current status bar content area and the current chip's measured size to update
-     * the animation rect and chipBounds. This method can be called at any time and will update
-     * the current animation values properly during e.g. a rotation.
+     * Use the current status bar content area and the current chip's measured size to update the
+     * animation rect and chipBounds. This method can be called at any time and will update the
+     * current animation values properly during e.g. a rotation.
      */
     private fun updateChipBounds(chip: BackgroundAnimatableView, contentArea: Rect) {
         // decide which direction we're animating from, and then set some screen coordinates
@@ -309,14 +364,13 @@
     }
 
     private fun layoutParamsDefault(marginEnd: Int): FrameLayout.LayoutParams =
-            FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).also {
-                it.gravity = Gravity.END or Gravity.CENTER_VERTICAL
-                it.marginEnd = marginEnd
-            }
+        FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).also {
+            it.gravity = Gravity.END or Gravity.CENTER_VERTICAL
+            it.marginEnd = marginEnd
+        }
 
     private fun initializeAnimRect() = animRect.set(chipBounds)
 
-
     /**
      * To be called during an animation, sets the width and updates the current animated chip view
      */
@@ -324,7 +378,8 @@
         when (animationDirection) {
             LEFT -> {
                 animRect.set((chipRight - width), animRect.top, chipRight, animRect.bottom)
-            } else /* RIGHT */ -> {
+            }
+            else /* RIGHT */ -> {
                 animRect.set(chipLeft, animRect.top, (chipLeft + width), animRect.bottom)
             }
         }
@@ -337,44 +392,73 @@
      */
     private fun updateAnimatedViewBoundsHeight(height: Int, verticalCenter: Int) {
         animRect.set(
-                animRect.left,
-                verticalCenter - (height.toFloat() / 2).roundToInt(),
-                animRect.right,
-                verticalCenter + (height.toFloat() / 2).roundToInt())
+            animRect.left,
+            verticalCenter - (height.toFloat() / 2).roundToInt(),
+            animRect.right,
+            verticalCenter + (height.toFloat() / 2).roundToInt(),
+        )
 
         updateCurrentAnimatedView()
     }
 
-    /**
-     * To be called during an animation, updates the animation rect offset and updates the chip
-     */
+    /** To be called during an animation, updates the animation rect offset and updates the chip */
     private fun updateAnimatedBoundsX(translation: Int) {
         currentAnimatedView?.view?.translationX = translation.toFloat()
     }
 
-    /**
-     * To be called during an animation. Sets the chip rect to animRect
-     */
+    /** To be called during an animation. Sets the chip rect to animRect */
     private fun updateCurrentAnimatedView() {
         currentAnimatedView?.setBoundsForAnimation(
-                animRect.left, animRect.top, animRect.right, animRect.bottom
+            animRect.left,
+            animRect.top,
+            animRect.right,
+            animRect.bottom,
         )
     }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            context: Context,
+            statusBarWindowController: StatusBarWindowController,
+            contentInsetsProvider: StatusBarContentInsetsProvider,
+        ): SystemEventChipAnimationControllerImpl
+    }
 }
 
-/**
- * Chips should provide a view that can be animated with something better than a fade-in
- */
+/** Chips should provide a view that can be animated with something better than a fade-in */
 interface BackgroundAnimatableView {
     val view: View // Since this can't extend View, add a view prop
         get() = this as View
+
     val contentView: View? // This will be alpha faded during appear and disappear animation
         get() = null
+
     val chipWidth: Int
         get() = view.measuredWidth
+
     fun setBoundsForAnimation(l: Int, t: Int, r: Int, b: Int)
 }
 
 // Animation directions
 private const val LEFT = 1
 private const val RIGHT = 2
+
+@Module
+object SystemEventChipAnimationControllerModule {
+
+    @Provides
+    @SysUISingleton
+    fun controller(
+        factory: SystemEventChipAnimationControllerImpl.Factory,
+        context: Context,
+        statusBarWindowControllerStore: StatusBarWindowControllerStore,
+        contentInsetsProvider: StatusBarContentInsetsProvider,
+    ): SystemEventChipAnimationController {
+        return factory.create(
+            context,
+            statusBarWindowControllerStore.defaultDisplay,
+            contentInsetsProvider,
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt
new file mode 100644
index 0000000..2ee1dffd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection
+
+import android.annotation.SuppressLint
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.Dumpable
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.printCollection
+import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.time.SystemClockImpl
+import com.android.systemui.util.withIncreasedIndent
+import java.io.PrintWriter
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * A cache in which entries can "survive" getting purged [retainCount] times, given consecutive
+ * [purge] calls made at least [purgeTimeoutMillis] apart. See also [purge].
+ *
+ * This cache is safe for multithreaded usage, and is recommended for objects that take a while to
+ * resolve (such as drawables, or things that require binder calls). As such, [getOrFetch] is
+ * recommended to be run on a background thread, while [purge] can be done from any thread.
+ */
+@SuppressLint("DumpableNotRegistered") // this will be dumped by container classes
+class NotifCollectionCache<V>(
+    private val retainCount: Int = 1,
+    private val purgeTimeoutMillis: Long = 1000L,
+    private val systemClock: SystemClock = SystemClockImpl(),
+) : Dumpable {
+    @get:VisibleForTesting val cache = ConcurrentHashMap<String, CacheEntry>()
+
+    // Counters for cache hits and misses to be used to calculate and dump the hit ratio
+    @get:VisibleForTesting val misses = AtomicInteger(0)
+    @get:VisibleForTesting val hits = AtomicInteger(0)
+
+    init {
+        if (retainCount < 0) {
+            throw IllegalArgumentException("retainCount cannot be negative")
+        }
+    }
+
+    inner class CacheEntry(val key: String, val value: V) {
+        /**
+         * The "lives" represent how many times the entry will remain in the cache when purging it
+         * is attempted.
+         */
+        @get:VisibleForTesting var lives: Int = retainCount + 1
+        /**
+         * The last time this entry lost a "life". Starts at a negative value chosen so that the
+         * first purge is always considered "valid".
+         */
+        private var lastValidPurge: Long = -purgeTimeoutMillis
+
+        fun resetLives() {
+            // Lives/timeouts don't matter if retainCount is 0
+            if (retainCount == 0) {
+                return
+            }
+
+            synchronized(key) {
+                lives = retainCount + 1
+                lastValidPurge = -purgeTimeoutMillis
+            }
+            // Add it to the cache again just in case it was deleted before we could reset the lives
+            cache[key] = this
+        }
+
+        fun tryPurge(): Boolean {
+            // Lives/timeouts don't matter if retainCount is 0
+            if (retainCount == 0) {
+                return true
+            }
+
+            // Using uptimeMillis since it's guaranteed to be monotonic, as we don't want a
+            // timezone/clock change to break us
+            val now = systemClock.uptimeMillis()
+
+            // Cannot purge the same entry from two threads simultaneously
+            synchronized(key) {
+                if (now - lastValidPurge < purgeTimeoutMillis) {
+                    return false
+                }
+                lastValidPurge = now
+                return --lives <= 0
+            }
+        }
+    }
+
+    /**
+     * Get value from cache, or fetch it and add it to cache if not found. This can be called from
+     * any thread, but is usually expected to be called from the background.
+     *
+     * @param key key for the object to be obtained
+     * @param fetch method to fetch the object and add it to the cache if not present; note that
+     *   there is no guarantee that two [fetch] cannot run in parallel for the same [key] (if
+     *   [getOrFetch] is called simultaneously from different threads), so be mindful of potential
+     *   side effects
+     */
+    fun getOrFetch(key: String, fetch: (String) -> V): V {
+        val entry = cache[key]
+        if (entry != null) {
+            hits.incrementAndGet()
+            // Refresh lives on access
+            entry.resetLives()
+            return entry.value
+        }
+
+        misses.incrementAndGet()
+        val value = fetch(key)
+        cache[key] = CacheEntry(key, value)
+        return value
+    }
+
+    /**
+     * Clear entries that are NOT in [wantedKeys] if appropriate. This can be called from any
+     * thread.
+     *
+     * If retainCount > 0, a given entry will need to not be present in [wantedKeys] for
+     * ([retainCount] + 1) consecutive [purge] calls made within at least [purgeTimeoutMillis] of
+     * each other in order to be cleared. This count will be reset for any given entry 1) if
+     * [getOrFetch] is called for the entry or 2) if the entry is present in [wantedKeys] in a
+     * subsequent [purge] call. We prioritize keeping the entry if possible, so if [purge] is called
+     * simultaneously with [getOrFetch] on different threads for example, we will try to keep it in
+     * the cache, although it is not guaranteed. If avoiding cache misses is a concern, consider
+     * increasing the [retainCount] or [purgeTimeoutMillis].
+     *
+     * For example, say [retainCount] = 1 and [purgeTimeoutMillis] = 1000 and we start with entries
+     * (a, b, c) in the cache:
+     * ```kotlin
+     * purge((a, c)); // marks b for deletion
+     * Thread.sleep(500)
+     * purge((a, c)); // does nothing as it was called earlier than the min 1s
+     * Thread.sleep(500)
+     * purge((b, c)); // b is no longer marked for deletion, but now a is
+     * Thread.sleep(1000);
+     * purge((c));    // deletes a from the cache and marks b for deletion, etc.
+     * ```
+     */
+    fun purge(wantedKeys: List<String>) {
+        for ((key, entry) in cache) {
+            if (key in wantedKeys) {
+                entry.resetLives()
+            } else if (entry.tryPurge()) {
+                cache.remove(key)
+            }
+        }
+    }
+
+    /** Clear all entries from the cache. */
+    fun clear() {
+        cache.clear()
+    }
+
+    override fun dump(pwOrig: PrintWriter, args: Array<out String>) {
+        val pw = pwOrig.asIndenting()
+
+        pw.println("$TAG(retainCount = $retainCount, purgeTimeoutMillis = $purgeTimeoutMillis)")
+        pw.withIncreasedIndent {
+            pw.printCollection("keys present in cache", cache.keys.stream().sorted().toList())
+
+            val misses = misses.get()
+            val hits = hits.get()
+            pw.println(
+                "cache hit ratio = ${(hits.toFloat() / (hits + misses)) * 100}% " +
+                    "($hits hits, $misses misses)"
+            )
+        }
+    }
+
+    companion object {
+        const val TAG = "NotifCollectionCache"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 38e6609..933f793 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -262,6 +262,11 @@
      */
     private boolean mIsHeadsUp;
 
+    /**
+     * Whether or not the notification is showing the app icon instead of the small icon.
+     */
+    private boolean mIsShowingAppIcon;
+
     private boolean mLastChronometerRunning = true;
     private ViewStub mChildrenContainerStub;
     private GroupMembershipManager mGroupMembershipManager;
@@ -816,6 +821,20 @@
         }
     }
 
+    /**
+     * Indicate that the notification is showing the app icon instead of the small icon.
+     */
+    public void setIsShowingAppIcon(boolean isShowingAppIcon) {
+        mIsShowingAppIcon = isShowingAppIcon;
+    }
+
+    /**
+     * Whether or not the notification is showing the app icon instead of the small icon.
+     */
+    public boolean isShowingAppIcon() {
+        return mIsShowingAppIcon;
+    }
+
     @Override
     public boolean showingPulsing() {
         return isHeadsUpState() && (isDozing() || (mOnKeyguard && isBypassEnabled()));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationRowIconViewInflaterFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationRowIconViewInflaterFactory.kt
index 79defd2..7b85bfd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationRowIconViewInflaterFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationRowIconViewInflaterFactory.kt
@@ -21,6 +21,7 @@
 import android.util.AttributeSet
 import android.view.View
 import com.android.internal.widget.NotificationRowIconView
+import com.android.internal.widget.NotificationRowIconView.NotificationIconProvider
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.NotifRemoteViewsFactory
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder
@@ -47,20 +48,27 @@
         return when (name) {
             NotificationRowIconView::class.java.name ->
                 NotificationRowIconView(context, attrs).also { view ->
-                    val sbn = row.entry.sbn
-                    view.setIconProvider(
-                        object : NotificationRowIconView.NotificationIconProvider {
-                            override fun shouldShowAppIcon(): Boolean {
-                                return iconStyleProvider.shouldShowAppIcon(row.entry.sbn, context)
-                            }
-
-                            override fun getAppIcon(): Drawable {
-                                return appIconProvider.getOrFetchAppIcon(sbn.packageName, context)
-                            }
-                        }
-                    )
+                    view.setIconProvider(createIconProvider(row, context))
                 }
             else -> null
         }
     }
+
+    private fun createIconProvider(
+        row: ExpandableNotificationRow,
+        context: Context,
+    ): NotificationIconProvider {
+        val sbn = row.entry.sbn
+        return object : NotificationIconProvider {
+            override fun shouldShowAppIcon(): Boolean {
+                val shouldShowAppIcon = iconStyleProvider.shouldShowAppIcon(row.entry.sbn, context)
+                row.setIsShowingAppIcon(shouldShowAppIcon)
+                return shouldShowAppIcon
+            }
+
+            override fun getAppIcon(): Drawable {
+                return appIconProvider.getOrFetchAppIcon(sbn.packageName, context)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
index b4411f1..f8aff69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
@@ -16,15 +16,18 @@
 
 package com.android.systemui.statusbar.notification.row.wrapper
 
+import android.app.Flags
 import android.content.Context
 import android.graphics.drawable.AnimatedImageDrawable
 import android.view.View
 import android.view.ViewGroup
+import android.view.ViewGroup.MarginLayoutParams
 import com.android.internal.widget.CachingIconView
 import com.android.internal.widget.ConversationLayout
 import com.android.internal.widget.MessagingGroup
 import com.android.internal.widget.MessagingImageMessage
 import com.android.internal.widget.MessagingLinearLayout
+import com.android.internal.widget.NotificationRowIconView
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.notification.NotificationFadeAware
 import com.android.systemui.statusbar.notification.NotificationUtils
@@ -32,23 +35,23 @@
 import com.android.systemui.statusbar.notification.row.wrapper.NotificationMessagingTemplateViewWrapper.setCustomImageMessageTransform
 import com.android.systemui.util.children
 
-/**
- * Wraps a notification containing a conversation template
- */
-class NotificationConversationTemplateViewWrapper constructor(
+/** Wraps a notification containing a conversation template */
+class NotificationConversationTemplateViewWrapper(
     ctx: Context,
     view: View,
-    row: ExpandableNotificationRow
+    row: ExpandableNotificationRow,
 ) : NotificationTemplateViewWrapper(ctx, view, row) {
 
-    private val minHeightWithActions: Int = NotificationUtils.getFontScaledHeight(
+    private val minHeightWithActions: Int =
+        NotificationUtils.getFontScaledHeight(
             ctx,
-            R.dimen.notification_messaging_actions_min_height
-    )
+            R.dimen.notification_messaging_actions_min_height,
+        )
     private val conversationLayout: ConversationLayout = view as ConversationLayout
 
     private lateinit var conversationIconContainer: View
     private lateinit var conversationIconView: CachingIconView
+    private lateinit var badgeIconView: NotificationRowIconView
     private lateinit var conversationBadgeBg: View
     private lateinit var expandBtn: View
     private lateinit var expandBtnContainer: View
@@ -68,10 +71,13 @@
         messageContainers = conversationLayout.messagingGroups
         with(conversationLayout) {
             conversationIconContainer =
-                    requireViewById(com.android.internal.R.id.conversation_icon_container)
+                requireViewById(com.android.internal.R.id.conversation_icon_container)
             conversationIconView = requireViewById(com.android.internal.R.id.conversation_icon)
+            if (Flags.notificationsRedesignAppIcons()) {
+                badgeIconView = requireViewById(com.android.internal.R.id.icon)
+            }
             conversationBadgeBg =
-                    requireViewById(com.android.internal.R.id.conversation_icon_badge_bg)
+                requireViewById(com.android.internal.R.id.conversation_icon_badge_bg)
             expandBtn = requireViewById(com.android.internal.R.id.expand_button)
             expandBtnContainer = requireViewById(com.android.internal.R.id.expand_button_container)
             importanceRing = requireViewById(com.android.internal.R.id.conversation_icon_badge_ring)
@@ -80,7 +86,7 @@
             facePileTop = findViewById(com.android.internal.R.id.conversation_face_pile_top)
             facePileBottom = findViewById(com.android.internal.R.id.conversation_face_pile_bottom)
             facePileBottomBg =
-                    findViewById(com.android.internal.R.id.conversation_face_pile_bottom_background)
+                findViewById(com.android.internal.R.id.conversation_face_pile_bottom_background)
         }
     }
 
@@ -88,6 +94,13 @@
         // Reinspect the notification. Before the super call, because the super call also updates
         // the transformation types and we need to have our values set by then.
         resolveViews()
+        if (Flags.notificationsRedesignAppIcons() && row.isShowingAppIcon) {
+            // Override the margins to be 2dp instead of 4dp according to the new design if we're
+            // showing the app icon.
+            val lp = badgeIconView.layoutParams as MarginLayoutParams
+            lp.setMargins(2, 2, 2, 2)
+            badgeIconView.layoutParams = lp
+        }
         super.onContentUpdated(row)
     }
 
@@ -96,56 +109,50 @@
         super.updateTransformedTypes()
 
         mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TITLE, conversationTitleView)
-        addTransformedViews(
-                messagingLinearLayout,
-                appName
-        )
+        addTransformedViews(messagingLinearLayout, appName)
 
         setCustomImageMessageTransform(mTransformationHelper, imageMessageContainer)
 
         addViewsTransformingToSimilar(
-                conversationIconView,
-                conversationBadgeBg,
-                expandBtn,
-                importanceRing,
-                facePileTop,
-                facePileBottom,
-                facePileBottomBg
+            conversationIconView,
+            conversationBadgeBg,
+            expandBtn,
+            importanceRing,
+            facePileTop,
+            facePileBottom,
+            facePileBottomBg,
         )
     }
 
     override fun getShelfTransformationTarget(): View? =
-            if (conversationLayout.isImportantConversation)
-                if (conversationIconView.visibility != View.GONE)
-                    conversationIconView
-                else
-                    // A notification with a fallback icon was set to important. Currently
-                    // the transformation doesn't work for these and needs to be fixed.
-                    // In the meantime those are using the icon.
-                    super.getShelfTransformationTarget()
+        if (conversationLayout.isImportantConversation)
+            if (conversationIconView.visibility != View.GONE) conversationIconView
             else
-                super.getShelfTransformationTarget()
+            // A notification with a fallback icon was set to important. Currently
+            // the transformation doesn't work for these and needs to be fixed.
+            // In the meantime those are using the icon.
+            super.getShelfTransformationTarget()
+        else super.getShelfTransformationTarget()
 
     override fun setRemoteInputVisible(visible: Boolean) =
-            conversationLayout.showHistoricMessages(visible)
+        conversationLayout.showHistoricMessages(visible)
 
     override fun updateExpandability(
         expandable: Boolean,
         onClickListener: View.OnClickListener,
-        requestLayout: Boolean
+        requestLayout: Boolean,
     ) = conversationLayout.updateExpandability(expandable, onClickListener)
 
     override fun disallowSingleClick(x: Float, y: Float): Boolean {
-        val isOnExpandButton = expandBtnContainer.visibility == View.VISIBLE &&
-                isOnView(expandBtnContainer, x, y)
+        val isOnExpandButton =
+            expandBtnContainer.visibility == View.VISIBLE && isOnView(expandBtnContainer, x, y)
         return isOnExpandButton || super.disallowSingleClick(x, y)
     }
 
     override fun getMinLayoutHeight(): Int =
-            if (mActionsContainer != null && mActionsContainer.visibility != View.GONE)
-                minHeightWithActions
-            else
-                super.getMinLayoutHeight()
+        if (mActionsContainer != null && mActionsContainer.visibility != View.GONE)
+            minHeightWithActions
+        else super.getMinLayoutHeight()
 
     override fun setNotificationFaded(faded: Boolean) {
         // Do not call super
@@ -157,16 +164,17 @@
     override fun setAnimationsRunning(running: Boolean) {
         // We apply to both the child message containers in a conversation group,
         // and the top level image message container.
-        val containers = messageContainers.asSequence().map { it.messageContainer } +
+        val containers =
+            messageContainers.asSequence().map { it.messageContainer } +
                 sequenceOf(imageMessageContainer)
         val drawables =
-                containers
-                        .flatMap { it.children }
-                        .mapNotNull { child ->
-                            (child as? MessagingImageMessage)?.let { imageMessage ->
-                                imageMessage.drawable as? AnimatedImageDrawable
-                            }
-                        }
+            containers
+                .flatMap { it.children }
+                .mapNotNull { child ->
+                    (child as? MessagingImageMessage)?.let { imageMessage ->
+                        imageMessage.drawable as? AnimatedImageDrawable
+                    }
+                }
         drawables.toSet().forEach {
             when {
                 running -> it.start()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderImplTest.kt
index d2a17c2..ad58a01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderImplTest.kt
@@ -37,14 +37,14 @@
         overrideResource(R.string.config_protectedPhysicalCameraId, OUTER_CAMERA_PHYSICAL_ID)
         overrideResource(
             R.string.config_frontBuiltInDisplayCutoutProtection,
-            OUTER_CAMERA_PROTECTION_PATH
+            OUTER_CAMERA_PROTECTION_PATH,
         )
         overrideResource(R.string.config_protectedScreenUniqueId, OUTER_SCREEN_UNIQUE_ID)
         overrideResource(R.string.config_protectedInnerCameraId, INNER_CAMERA_LOGICAL_ID)
         overrideResource(R.string.config_protectedInnerPhysicalCameraId, INNER_CAMERA_PHYSICAL_ID)
         overrideResource(
             R.string.config_innerBuiltInDisplayCutoutProtection,
-            INNER_CAMERA_PROTECTION_PATH
+            INNER_CAMERA_PROTECTION_PATH,
         )
         overrideResource(R.string.config_protectedInnerScreenUniqueId, INNER_SCREEN_UNIQUE_ID)
     }
@@ -107,7 +107,7 @@
         private const val OUTER_CAMERA_PHYSICAL_ID = "11"
         private const val OUTER_CAMERA_PROTECTION_PATH = "M 0,0 H 10,10 V 10,10 H 0,10 Z"
         private val OUTER_CAMERA_PROTECTION_BOUNDS =
-            Rect(/* left = */ 0, /* top = */ 0, /* right = */ 10, /* bottom = */ 10)
+            Rect(/* left= */ 0, /* top= */ 0, /* right= */ 10, /* bottom= */ 10)
         private const val OUTER_SCREEN_UNIQUE_ID = "111"
         private val OUTER_CAMERA_PROTECTION_INFO =
             TestableProtectionInfo(
@@ -121,7 +121,7 @@
         private const val INNER_CAMERA_PHYSICAL_ID = "22"
         private const val INNER_CAMERA_PROTECTION_PATH = "M 0,0 H 20,20 V 20,20 H 0,20 Z"
         private val INNER_CAMERA_PROTECTION_BOUNDS =
-            Rect(/* left = */ 0, /* top = */ 0, /* right = */ 20, /* bottom = */ 20)
+            Rect(/* left= */ 0, /* top= */ 0, /* right= */ 20, /* bottom= */ 20)
         private const val INNER_SCREEN_UNIQUE_ID = "222"
         private val INNER_CAMERA_PROTECTION_INFO =
             TestableProtectionInfo(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt
index bc12aaa..a01feca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
 import com.android.systemui.decor.FaceScanningProviderFactory
+import com.android.systemui.decor.FaceScanningProviderFactoryImpl
 import com.android.systemui.log.ScreenDecorationsLogger
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -69,20 +70,20 @@
                 dmGlobal,
                 displayId,
                 displayInfo,
-                DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS
+                DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS,
             )
         whenever(dmGlobal.getDisplayInfo(eq(displayId))).thenReturn(displayInfo)
         val displayContext = context.createDisplayContext(display) as SysuiTestableContext
         displayContext.orCreateTestableResources.addOverride(
             R.array.config_displayUniqueIdArray,
-            arrayOf(displayId)
+            arrayOf(displayId),
         )
         displayContext.orCreateTestableResources.addOverride(
             R.bool.config_fillMainBuiltInDisplayCutout,
-            true
+            true,
         )
         underTest =
-            FaceScanningProviderFactory(
+            FaceScanningProviderFactoryImpl(
                 authController,
                 displayContext,
                 statusBarStateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/SysUICutoutProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/SysUICutoutProviderTest.kt
index 61c7e1d..ef33210 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/SysUICutoutProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/SysUICutoutProviderTest.kt
@@ -42,7 +42,7 @@
     fun cutoutInfoForCurrentDisplay_noCutout_returnsNull() {
         val noCutoutDisplay = createDisplay(cutout = null)
         val noCutoutDisplayContext = context.createDisplayContext(noCutoutDisplay)
-        val provider = SysUICutoutProvider(noCutoutDisplayContext, fakeProtectionLoader)
+        val provider = SysUICutoutProviderImpl(noCutoutDisplayContext, fakeProtectionLoader)
 
         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()
 
@@ -53,7 +53,7 @@
     fun cutoutInfoForCurrentDisplay_returnsCutout() {
         val cutoutDisplay = createDisplay()
         val cutoutDisplayContext = context.createDisplayContext(cutoutDisplay)
-        val provider = SysUICutoutProvider(cutoutDisplayContext, fakeProtectionLoader)
+        val provider = SysUICutoutProviderImpl(cutoutDisplayContext, fakeProtectionLoader)
 
         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
 
@@ -64,7 +64,7 @@
     fun cutoutInfoForCurrentDisplay_noAssociatedProtection_returnsNoProtection() {
         val cutoutDisplay = createDisplay()
         val cutoutDisplayContext = context.createDisplayContext(cutoutDisplay)
-        val provider = SysUICutoutProvider(cutoutDisplayContext, fakeProtectionLoader)
+        val provider = SysUICutoutProviderImpl(cutoutDisplayContext, fakeProtectionLoader)
 
         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
 
@@ -75,7 +75,7 @@
     fun cutoutInfoForCurrentDisplay_outerDisplay_protectionAssociated_returnsProtection() {
         fakeProtectionLoader.addOuterCameraProtection(displayUniqueId = OUTER_DISPLAY_UNIQUE_ID)
         val outerDisplayContext = context.createDisplayContext(OUTER_DISPLAY)
-        val provider = SysUICutoutProvider(outerDisplayContext, fakeProtectionLoader)
+        val provider = SysUICutoutProviderImpl(outerDisplayContext, fakeProtectionLoader)
 
         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
 
@@ -86,7 +86,7 @@
     fun cutoutInfoForCurrentDisplay_outerDisplay_protectionNotAvailable_returnsNullProtection() {
         fakeProtectionLoader.clearProtectionInfoList()
         val outerDisplayContext = context.createDisplayContext(OUTER_DISPLAY)
-        val provider = SysUICutoutProvider(outerDisplayContext, fakeProtectionLoader)
+        val provider = SysUICutoutProviderImpl(outerDisplayContext, fakeProtectionLoader)
 
         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
 
@@ -97,7 +97,7 @@
     fun cutoutInfoForCurrentDisplay_displayWithNullId_protectionsWithNoId_returnsNullProtection() {
         fakeProtectionLoader.addOuterCameraProtection(displayUniqueId = "")
         val displayContext = context.createDisplayContext(createDisplay(uniqueId = null))
-        val provider = SysUICutoutProvider(displayContext, fakeProtectionLoader)
+        val provider = SysUICutoutProviderImpl(displayContext, fakeProtectionLoader)
 
         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
 
@@ -108,7 +108,7 @@
     fun cutoutInfoForCurrentDisplay_displayWithEmptyId_protectionsWithNoId_returnsNullProtection() {
         fakeProtectionLoader.addOuterCameraProtection(displayUniqueId = "")
         val displayContext = context.createDisplayContext(createDisplay(uniqueId = ""))
-        val provider = SysUICutoutProvider(displayContext, fakeProtectionLoader)
+        val provider = SysUICutoutProviderImpl(displayContext, fakeProtectionLoader)
 
         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
 
@@ -123,15 +123,13 @@
                 displayHeight = 1000,
                 rotation = Surface.ROTATION_0,
                 protectionBounds =
-                    Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
+                    Rect(/* left= */ 440, /* top= */ 10, /* right= */ 490, /* bottom= */ 110),
             )
 
         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
 
         assertThat(sysUICutout.cameraProtection!!.bounds)
-            .isEqualTo(
-                Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
-            )
+            .isEqualTo(Rect(/* left= */ 440, /* top= */ 10, /* right= */ 490, /* bottom= */ 110))
     }
 
     @Test
@@ -142,13 +140,13 @@
                 displayHeight = 1000,
                 rotation = Surface.ROTATION_90,
                 protectionBounds =
-                    Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
+                    Rect(/* left= */ 440, /* top= */ 10, /* right= */ 490, /* bottom= */ 110),
             )
 
         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
 
         assertThat(sysUICutout.cameraProtection!!.bounds)
-            .isEqualTo(Rect(/* left = */ 10, /* top = */ 10, /* right = */ 110, /* bottom = */ 60))
+            .isEqualTo(Rect(/* left= */ 10, /* top= */ 10, /* right= */ 110, /* bottom= */ 60))
     }
 
     @Test
@@ -156,7 +154,7 @@
         val displayNaturalWidth = 500
         val displayNaturalHeight = 1000
         val originalProtectionBounds =
-            Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
+            Rect(/* left= */ 440, /* top= */ 10, /* right= */ 490, /* bottom= */ 110)
         // Safe copy as we don't know at which layer the mutation could happen
         val originalProtectionBoundsCopy = Rect(originalProtectionBounds)
         val display =
@@ -168,10 +166,10 @@
             )
         fakeProtectionLoader.addOuterCameraProtection(
             displayUniqueId = OUTER_DISPLAY_UNIQUE_ID,
-            bounds = originalProtectionBounds
+            bounds = originalProtectionBounds,
         )
         val provider =
-            SysUICutoutProvider(context.createDisplayContext(display), fakeProtectionLoader)
+            SysUICutoutProviderImpl(context.createDisplayContext(display), fakeProtectionLoader)
 
         // Here we get the rotated bounds once
         provider.cutoutInfoForCurrentDisplayAndRotation()
@@ -194,13 +192,13 @@
                 displayHeight = 1000,
                 rotation = Surface.ROTATION_180,
                 protectionBounds =
-                    Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
+                    Rect(/* left= */ 440, /* top= */ 10, /* right= */ 490, /* bottom= */ 110),
             )
 
         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
 
         assertThat(sysUICutout.cameraProtection!!.bounds)
-            .isEqualTo(Rect(/* left = */ 10, /* top = */ 890, /* right = */ 60, /* bottom = */ 990))
+            .isEqualTo(Rect(/* left= */ 10, /* top= */ 890, /* right= */ 60, /* bottom= */ 990))
     }
 
     @Test
@@ -211,15 +209,13 @@
                 displayHeight = 1000,
                 rotation = Surface.ROTATION_270,
                 protectionBounds =
-                    Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
+                    Rect(/* left= */ 440, /* top= */ 10, /* right= */ 490, /* bottom= */ 110),
             )
 
         val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
 
         assertThat(sysUICutout.cameraProtection!!.bounds)
-            .isEqualTo(
-                Rect(/* left = */ 890, /* top = */ 440, /* right = */ 990, /* bottom = */ 490)
-            )
+            .isEqualTo(Rect(/* left= */ 890, /* top= */ 440, /* right= */ 990, /* bottom= */ 490))
     }
 
     private fun setUpProviderWithCameraProtection(
@@ -245,9 +241,9 @@
             )
         fakeProtectionLoader.addOuterCameraProtection(
             displayUniqueId = OUTER_DISPLAY_UNIQUE_ID,
-            bounds = protectionBounds
+            bounds = protectionBounds,
         )
-        return SysUICutoutProvider(context.createDisplayContext(display), fakeProtectionLoader)
+        return SysUICutoutProviderImpl(context.createDisplayContext(display), fakeProtectionLoader)
     }
 
     companion object {
@@ -259,7 +255,7 @@
             height: Int = 1000,
             @Rotation rotation: Int = Surface.ROTATION_0,
             uniqueId: String? = "uniqueId",
-            cutout: DisplayCutout? = mock<DisplayCutout>()
+            cutout: DisplayCutout? = mock<DisplayCutout>(),
         ) =
             mock<Display> {
                 whenever(this.getDisplayInfo(any())).thenAnswer {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
index fd550b0..6e36d42b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
@@ -88,7 +88,7 @@
                     },
                 layout = BouncerSceneLayout.BESIDE_USER_SWITCHER,
                 modifier = Modifier.fillMaxSize().testTag("BouncerContent"),
-                dialogFactory = bouncerDialogFactory
+                dialogFactory = bouncerDialogFactory,
             )
         }
     }
@@ -110,11 +110,19 @@
                             }
                         }
                     ) {
-                        feature(hasTestTag("UserSwitcher"), positionInRoot, "userSwitcher_pos")
-                        feature(hasTestTag("UserSwitcher"), alpha, "userSwitcher_alpha")
+                        feature(
+                            hasTestTag("com.android.systemui:id/UserSwitcher"),
+                            positionInRoot,
+                            "userSwitcher_pos",
+                        )
+                        feature(
+                            hasTestTag("com.android.systemui:id/UserSwitcher"),
+                            alpha,
+                            "userSwitcher_alpha",
+                        )
                         feature(hasTestTag("FoldAware"), positionInRoot, "foldAware_pos")
                         feature(hasTestTag("FoldAware"), alpha, "foldAware_alpha")
-                    }
+                    },
                 )
 
             assertThat(motion).timeSeriesMatchesGolden()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 8731853..63ec78fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -35,6 +35,8 @@
 import android.app.WallpaperColors;
 import android.graphics.Bitmap;
 import android.graphics.drawable.Icon;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.testing.TestableLooper;
 import android.view.View;
 import android.widget.LinearLayout;
@@ -44,6 +46,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.media.flags.Flags;
 import com.android.settingslib.media.LocalMediaManager;
 import com.android.settingslib.media.MediaDevice;
 import com.android.systemui.SysuiTestCase;
@@ -738,4 +741,68 @@
 
         assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(updatedList.size());
     }
+
+    @DisableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+    @Test
+    public void getDrawableId_FlagDisabled_InputDeviceMutedIcon() {
+        assertThat(
+                mViewHolder.getDrawableId(true /* isInputDevice */, true /* isMutedVolumeIcon */))
+                .isEqualTo(R.drawable.media_output_icon_volume_off);
+    }
+
+    @DisableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+    @Test
+    public void getDrawableId_FlagDisabled_OutputDeviceMutedIcon() {
+        assertThat(
+                mViewHolder.getDrawableId(false /* isInputDevice */, true /* isMutedVolumeIcon */))
+                .isEqualTo(R.drawable.media_output_icon_volume_off);
+    }
+
+    @DisableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+    @Test
+    public void getDrawableId_FlagDisabled_InputDeviceUnmutedIcon() {
+        assertThat(
+                mViewHolder.getDrawableId(true /* isInputDevice */, false /* isMutedVolumeIcon */))
+                .isEqualTo(R.drawable.media_output_icon_volume);
+    }
+
+    @DisableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+    @Test
+    public void getDrawableId_FlagDisabled_OutputDeviceUnmutedIcon() {
+        assertThat(
+                mViewHolder.getDrawableId(false /* isInputDevice */, false /* isMutedVolumeIcon */))
+                .isEqualTo(R.drawable.media_output_icon_volume);
+    }
+
+    @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+    @Test
+    public void getDrawableId_FlagEnabled_InputDeviceMutedIcon() {
+        assertThat(
+                mViewHolder.getDrawableId(true /* isInputDevice */, true /* isMutedVolumeIcon */))
+                .isEqualTo(R.drawable.ic_mic_off);
+    }
+
+    @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+    @Test
+    public void getDrawableId_FlagEnabled_OutputDeviceMutedIcon() {
+        assertThat(
+                mViewHolder.getDrawableId(false /* isInputDevice */, true /* isMutedVolumeIcon */))
+                .isEqualTo(R.drawable.media_output_icon_volume_off);
+    }
+
+    @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+    @Test
+    public void getDrawableId_FlagEnabled_InputDeviceUnmutedIcon() {
+        assertThat(
+                mViewHolder.getDrawableId(true /* isInputDevice */, false /* isMutedVolumeIcon */))
+                .isEqualTo(R.drawable.ic_mic_26dp);
+    }
+
+    @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+    @Test
+    public void getDrawableId_FlagEnabled_OutputDeviceUnmutedIcon() {
+        assertThat(
+                mViewHolder.getDrawableId(false /* isInputDevice */, false /* isMutedVolumeIcon */))
+                .isEqualTo(R.drawable.media_output_icon_volume);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
index 5d8a8fd..4e7de81 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
@@ -88,30 +88,26 @@
             .thenReturn(statusBarWindowController)
         systemClock = FakeSystemClock()
         chipAnimationController =
-            SystemEventChipAnimationController(
+            SystemEventChipAnimationControllerImpl(
                 mContext,
-                statusBarWindowControllerStore,
+                statusBarWindowController,
                 statusBarContentInsetProvider,
             )
 
         // StatusBarContentInsetProvider is mocked. Ensure that it returns some mocked values.
         whenever(statusBarContentInsetProvider.getStatusBarContentInsetsForCurrentRotation())
-            .thenReturn(
-                Insets.of(/* left = */ 10, /* top = */ 10, /* right = */ 10, /* bottom = */ 0)
-            )
+            .thenReturn(Insets.of(/* left= */ 10, /* top= */ 10, /* right= */ 10, /* bottom= */ 0))
         whenever(statusBarContentInsetProvider.getStatusBarContentAreaForCurrentRotation())
-            .thenReturn(
-                Rect(/* left = */ 10, /* top = */ 10, /* right = */ 990, /* bottom = */ 100)
-            )
+            .thenReturn(Rect(/* left= */ 10, /* top= */ 10, /* right= */ 990, /* bottom= */ 100))
 
         // StatusBarWindowController is mocked. The addViewToWindow function needs to be mocked to
         // ensure that the chip view is added to a parent view
         whenever(statusBarWindowController.addViewToWindow(any(), any())).then {
             val statusbarFake = FrameLayout(mContext)
-            statusbarFake.layout(/* l = */ 0, /* t = */ 0, /* r = */ 1000, /* b = */ 100)
+            statusbarFake.layout(/* l= */ 0, /* t= */ 0, /* r= */ 1000, /* b= */ 100)
             statusbarFake.addView(
                 it.arguments[0] as View,
-                it.arguments[1] as FrameLayout.LayoutParams
+                it.arguments[1] as FrameLayout.LayoutParams,
             )
         }
     }
@@ -386,7 +382,7 @@
         scheduleFakeEventWithView(
             accessibilityDesc,
             mockAnimatableView,
-            shouldAnnounceAccessibilityEvent = true
+            shouldAnnounceAccessibilityEvent = true,
         )
         fastForwardAnimationToState(ANIMATING_OUT)
 
@@ -405,7 +401,7 @@
         scheduleFakeEventWithView(
             accessibilityDesc,
             mockAnimatableView,
-            shouldAnnounceAccessibilityEvent = true
+            shouldAnnounceAccessibilityEvent = true,
         )
         fastForwardAnimationToState(ANIMATING_OUT)
 
@@ -424,7 +420,7 @@
         scheduleFakeEventWithView(
             accessibilityDesc,
             mockAnimatableView,
-            shouldAnnounceAccessibilityEvent = false
+            shouldAnnounceAccessibilityEvent = false,
         )
         fastForwardAnimationToState(ANIMATING_OUT)
 
@@ -637,13 +633,13 @@
     private fun scheduleFakeEventWithView(
         desc: String?,
         view: BackgroundAnimatableView,
-        shouldAnnounceAccessibilityEvent: Boolean
+        shouldAnnounceAccessibilityEvent: Boolean,
     ) {
         val fakeEvent =
             FakeStatusEvent(
                 viewCreator = { view },
                 contentDescription = desc,
-                shouldAnnounceAccessibilityEvent = shouldAnnounceAccessibilityEvent
+                shouldAnnounceAccessibilityEvent = shouldAnnounceAccessibilityEvent,
             )
         systemStatusAnimationScheduler.onStatusEvent(fakeEvent)
     }
@@ -668,7 +664,7 @@
                 dumpManager,
                 systemClock,
                 CoroutineScope(StandardTestDispatcher(testScope.testScheduler)),
-                logger
+                logger,
             )
         // add a mock listener
         systemStatusAnimationScheduler.addCallback(listener)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt
deleted file mode 100644
index 3190171..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2024 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.keyboard.shortcut
-
-import android.content.Intent
-
-class FakeShortcutHelperStartActivity : (Intent) -> Unit {
-
-    val startIntents = mutableListOf<Intent>()
-
-    override fun invoke(intent: Intent) {
-        startIntents += intent
-    }
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
index c2a03d4..fbfaba6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -33,7 +33,6 @@
 import com.android.systemui.keyboard.shortcut.data.source.SystemShortcutsSource
 import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCategoriesInteractor
 import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperStateInteractor
-import com.android.systemui.keyboard.shortcut.ui.ShortcutHelperActivityStarter
 import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
 import com.android.systemui.keyguard.data.repository.fakeCommandQueue
 import com.android.systemui.kosmos.Kosmos
@@ -45,12 +44,7 @@
 import com.android.systemui.settings.fakeUserTracker
 
 var Kosmos.shortcutHelperAppCategoriesShortcutsSource: KeyboardShortcutGroupsSource by
-    Kosmos.Fixture {
-        AppCategoriesShortcutsSource(
-            windowManager,
-            testDispatcher,
-        )
-    }
+    Kosmos.Fixture { AppCategoriesShortcutsSource(windowManager, testDispatcher) }
 
 var Kosmos.shortcutHelperSystemShortcutsSource: KeyboardShortcutGroupsSource by
     Kosmos.Fixture { SystemShortcutsSource(mainResources) }
@@ -65,7 +59,7 @@
             broadcastDispatcher,
             fakeInputManager.inputManager,
             testScope,
-            testDispatcher
+            testDispatcher,
         )
     }
 
@@ -109,7 +103,7 @@
             displayTracker,
             testScope,
             sysUiState,
-            shortcutHelperStateRepository
+            shortcutHelperStateRepository,
         )
     }
 
@@ -124,18 +118,6 @@
             applicationCoroutineScope,
             testDispatcher,
             shortcutHelperStateInteractor,
-            shortcutHelperCategoriesInteractor
-        )
-    }
-
-val Kosmos.fakeShortcutHelperStartActivity by Kosmos.Fixture { FakeShortcutHelperStartActivity() }
-
-val Kosmos.shortcutHelperActivityStarter by
-    Kosmos.Fixture {
-        ShortcutHelperActivityStarter(
-            applicationContext,
-            applicationCoroutineScope,
-            shortcutHelperViewModel,
-            fakeShortcutHelperStartActivity,
+            shortcutHelperCategoriesInteractor,
         )
     }
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
index e2d73d1..9a145cb 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -137,9 +137,6 @@
 
     private static RavenwoodConfig sConfig;
     private static RavenwoodSystemProperties sProps;
-    // TODO: use the real UiAutomation class instead of a mock
-    private static UiAutomation sMockUiAutomation;
-    private static Set<String> sAdoptedPermissions = Collections.emptySet();
     private static boolean sInitialized = false;
 
     /**
@@ -187,7 +184,6 @@
                 "androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner");
 
         assertMockitoVersion();
-        sMockUiAutomation = createMockUiAutomation();
     }
 
     /**
@@ -273,7 +269,7 @@
 
         // Prepare other fields.
         config.mInstrumentation = new Instrumentation();
-        config.mInstrumentation.basicInit(instContext, targetContext, sMockUiAutomation);
+        config.mInstrumentation.basicInit(instContext, targetContext, createMockUiAutomation());
         InstrumentationRegistry.registerInstance(config.mInstrumentation, Bundle.EMPTY);
 
         RavenwoodSystemServer.init(config);
@@ -318,7 +314,6 @@
             ((RavenwoodContext) config.mTargetContext).cleanUp();
             config.mTargetContext = null;
         }
-        sMockUiAutomation.dropShellPermissionIdentity();
 
         Looper.getMainLooper().quit();
         Looper.clearMainLooperForTest();
@@ -421,28 +416,30 @@
                 () -> Class.forName("org.mockito.Matchers"));
     }
 
+    // TODO: use the real UiAutomation class instead of a mock
     private static UiAutomation createMockUiAutomation() {
+        final Set[] adoptedPermission = { Collections.emptySet() };
         var mock = mock(UiAutomation.class, inv -> {
             HostTestUtils.onThrowMethodCalled();
             return null;
         });
         doAnswer(inv -> {
-            sAdoptedPermissions = UiAutomation.ALL_PERMISSIONS;
+            adoptedPermission[0] = UiAutomation.ALL_PERMISSIONS;
             return null;
         }).when(mock).adoptShellPermissionIdentity();
         doAnswer(inv -> {
             if (inv.getArgument(0) == null) {
-                sAdoptedPermissions = UiAutomation.ALL_PERMISSIONS;
+                adoptedPermission[0] = UiAutomation.ALL_PERMISSIONS;
             } else {
-                sAdoptedPermissions = (Set) Set.of(inv.getArguments());
+                adoptedPermission[0] = Set.of(inv.getArguments());
             }
             return null;
         }).when(mock).adoptShellPermissionIdentity(any());
         doAnswer(inv -> {
-            sAdoptedPermissions = Collections.emptySet();
+            adoptedPermission[0] = Collections.emptySet();
             return null;
         }).when(mock).dropShellPermissionIdentity();
-        doAnswer(inv -> sAdoptedPermissions).when(mock).getAdoptedShellPermissions();
+        doAnswer(inv -> adoptedPermission[0]).when(mock).getAdoptedShellPermissions();
         return mock;
     }
 
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index 7057cc3..cb4e994 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -192,6 +192,16 @@
 }
 
 flag {
+    name: "package_monitor_dedicated_thread"
+    namespace: "accessibility"
+    description: "Runs the A11yManagerService PackageMonitor on a dedicated thread"
+    bug: "348138695"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "manager_package_monitor_logic_fix"
     namespace: "accessibility"
     description: "Corrects the return values of the HandleForceStop function"
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index ec8908b..c6fe497 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -116,6 +116,7 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
@@ -901,7 +902,19 @@
     private void registerBroadcastReceivers() {
         // package changes
         mPackageMonitor = new ManagerPackageMonitor(this);
-        mPackageMonitor.register(mContext, null,  UserHandle.ALL, true);
+        final Looper packageMonitorLooper;
+        if (Flags.packageMonitorDedicatedThread()) {
+            // Use a dedicated thread because the default BackgroundThread used by PackageMonitor
+            // is shared by other components and can get busy, causing a delay and eventual ANR when
+            // responding to broadcasts sent to this PackageMonitor.
+            HandlerThread packageMonitorThread = new HandlerThread(LOG_TAG + " PackageMonitor",
+                    Process.THREAD_PRIORITY_BACKGROUND);
+            packageMonitorThread.start();
+            packageMonitorLooper = packageMonitorThread.getLooper();
+        } else {
+            packageMonitorLooper = null;
+        }
+        mPackageMonitor.register(mContext, packageMonitorLooper,  UserHandle.ALL, true);
 
         // user change and unlock
         IntentFilter intentFilter = new IntentFilter();
diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
index 74908a4..3608360 100644
--- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
+++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
@@ -57,8 +57,11 @@
     /** Association id -> Transport */
     @GuardedBy("mTransports")
     private final SparseArray<Transport> mTransports = new SparseArray<>();
+
+    // Use mTransports to synchronize both mTransports and mTransportsListeners to avoid deadlock
+    // between threads that access both
     @NonNull
-    @GuardedBy("mTransportsListeners")
+    @GuardedBy("mTransports")
     private final RemoteCallbackList<IOnTransportsChangedListener> mTransportsListeners =
             new RemoteCallbackList<>();
 
@@ -95,7 +98,7 @@
      */
     public void addListener(IOnTransportsChangedListener listener) {
         Slog.i(TAG, "Registering OnTransportsChangedListener");
-        synchronized (mTransportsListeners) {
+        synchronized (mTransports) {
             mTransportsListeners.register(listener);
             mTransportsListeners.broadcast(listener1 -> {
                 // callback to the current listener with all the associations of the transports
@@ -114,7 +117,7 @@
      * Remove the listener for receiving callbacks when any of the transports is changed
      */
     public void removeListener(IOnTransportsChangedListener listener) {
-        synchronized (mTransportsListeners) {
+        synchronized (mTransports) {
             mTransportsListeners.unregister(listener);
         }
     }
@@ -204,7 +207,7 @@
     }
 
     private void notifyOnTransportsChanged() {
-        synchronized (mTransportsListeners) {
+        synchronized (mTransports) {
             mTransportsListeners.broadcast(listener -> {
                 try {
                     listener.onTransportsChanged(getAssociationsWithTransport());
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 6405ebb..217ef20 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -2757,8 +2757,11 @@
         if (isolated) {
             if (mIsolatedAppBindArgs == null) {
                 mIsolatedAppBindArgs = new ArrayMap<>(1);
+                // See b/79378449 about the following exemption.
                 addServiceToMap(mIsolatedAppBindArgs, "package");
-                addServiceToMap(mIsolatedAppBindArgs, "permissionmgr");
+                if (!android.server.Flags.removeJavaServiceManagerCache()) {
+                    addServiceToMap(mIsolatedAppBindArgs, "permissionmgr");
+                }
             }
             return mIsolatedAppBindArgs;
         }
@@ -2769,27 +2772,33 @@
             // Add common services.
             // IMPORTANT: Before adding services here, make sure ephemeral apps can access them too.
             // Enable the check in ApplicationThread.bindApplication() to make sure.
+            if (!android.server.Flags.removeJavaServiceManagerCache()) {
+                addServiceToMap(mAppBindArgs, "permissionmgr");
+                addServiceToMap(mAppBindArgs, Context.ALARM_SERVICE);
+                addServiceToMap(mAppBindArgs, Context.DISPLAY_SERVICE);
+                addServiceToMap(mAppBindArgs, Context.NETWORKMANAGEMENT_SERVICE);
+                addServiceToMap(mAppBindArgs, Context.CONNECTIVITY_SERVICE);
+                addServiceToMap(mAppBindArgs, Context.ACCESSIBILITY_SERVICE);
+                addServiceToMap(mAppBindArgs, Context.INPUT_METHOD_SERVICE);
+                addServiceToMap(mAppBindArgs, Context.INPUT_SERVICE);
+                addServiceToMap(mAppBindArgs, "graphicsstats");
+                addServiceToMap(mAppBindArgs, Context.APP_OPS_SERVICE);
+                addServiceToMap(mAppBindArgs, "content");
+                addServiceToMap(mAppBindArgs, Context.JOB_SCHEDULER_SERVICE);
+                addServiceToMap(mAppBindArgs, Context.NOTIFICATION_SERVICE);
+                addServiceToMap(mAppBindArgs, Context.VIBRATOR_SERVICE);
+                addServiceToMap(mAppBindArgs, Context.ACCOUNT_SERVICE);
+                addServiceToMap(mAppBindArgs, Context.POWER_SERVICE);
+                addServiceToMap(mAppBindArgs, Context.USER_SERVICE);
+                addServiceToMap(mAppBindArgs, "mount");
+                addServiceToMap(mAppBindArgs, Context.PLATFORM_COMPAT_SERVICE);
+            }
+            // See b/79378449
+            // Getting the window service and package service binder from servicemanager
+            // is blocked for Apps. However they are necessary for apps.
+            // TODO: remove exception
             addServiceToMap(mAppBindArgs, "package");
-            addServiceToMap(mAppBindArgs, "permissionmgr");
             addServiceToMap(mAppBindArgs, Context.WINDOW_SERVICE);
-            addServiceToMap(mAppBindArgs, Context.ALARM_SERVICE);
-            addServiceToMap(mAppBindArgs, Context.DISPLAY_SERVICE);
-            addServiceToMap(mAppBindArgs, Context.NETWORKMANAGEMENT_SERVICE);
-            addServiceToMap(mAppBindArgs, Context.CONNECTIVITY_SERVICE);
-            addServiceToMap(mAppBindArgs, Context.ACCESSIBILITY_SERVICE);
-            addServiceToMap(mAppBindArgs, Context.INPUT_METHOD_SERVICE);
-            addServiceToMap(mAppBindArgs, Context.INPUT_SERVICE);
-            addServiceToMap(mAppBindArgs, "graphicsstats");
-            addServiceToMap(mAppBindArgs, Context.APP_OPS_SERVICE);
-            addServiceToMap(mAppBindArgs, "content");
-            addServiceToMap(mAppBindArgs, Context.JOB_SCHEDULER_SERVICE);
-            addServiceToMap(mAppBindArgs, Context.NOTIFICATION_SERVICE);
-            addServiceToMap(mAppBindArgs, Context.VIBRATOR_SERVICE);
-            addServiceToMap(mAppBindArgs, Context.ACCOUNT_SERVICE);
-            addServiceToMap(mAppBindArgs, Context.POWER_SERVICE);
-            addServiceToMap(mAppBindArgs, Context.USER_SERVICE);
-            addServiceToMap(mAppBindArgs, "mount");
-            addServiceToMap(mAppBindArgs, Context.PLATFORM_COMPAT_SERVICE);
         }
         return mAppBindArgs;
     }
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 592d89e..c47cad9 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -196,6 +196,7 @@
     private final PowerAttributor mPowerAttributor;
 
     private volatile boolean mMonitorEnabled = true;
+    private boolean mRailsStatsCollectionEnabled = true;
 
     private native void getRailEnergyPowerStats(RailStats railStats);
     private CharsetDecoder mDecoderStat = StandardCharsets.UTF_8
@@ -312,8 +313,17 @@
         }
     }
 
+    public void setRailsStatsCollectionEnabled(boolean railsStatsCollectionEnabled) {
+        mRailsStatsCollectionEnabled = railsStatsCollectionEnabled;
+    }
+
     @Override
     public void fillRailDataStats(RailStats railStats) {
+        if (!mRailsStatsCollectionEnabled) {
+            railStats.setRailStatsAvailability(false);
+            return;
+        }
+
         if (DBG) Slog.d(TAG, "begin getRailEnergyPowerStats");
         try {
             getRailEnergyPowerStats(railStats);
@@ -423,7 +433,7 @@
         mStats = new BatteryStatsImpl(mBatteryStatsConfig, Clock.SYSTEM_CLOCK, mMonotonicClock,
                 systemDir, mHandler, this, this, mUserManagerUserInfoProvider, mPowerProfile,
                 mCpuScalingPolicies, mPowerStatsUidResolver);
-        mWorker = new BatteryExternalStatsWorker(context, mStats);
+        mWorker = new BatteryExternalStatsWorker(context, mStats, mHandler);
         mStats.setExternalStatsSyncLocked(mWorker);
         mStats.setRadioScanningTimeoutLocked(mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_radioScanningTimeout) * 1000L);
@@ -436,9 +446,12 @@
                 mCpuScalingPolicies, () -> mStats.getBatteryCapacity(),
                 mPowerStatsUidResolver);
         mPowerStatsScheduler = createPowerStatsScheduler(mContext);
+
+        int accumulatedBatteryUsageStatsSpanSize = mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_accumulatedBatteryUsageStatsSpanSize);
         mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context,
                 mPowerAttributor, mPowerProfile, mCpuScalingPolicies,
-                mPowerStatsStore, Clock.SYSTEM_CLOCK);
+                mPowerStatsStore, accumulatedBatteryUsageStatsSpanSize, Clock.SYSTEM_CLOCK);
         mDumpHelper = new BatteryStatsDumpHelperImpl(mBatteryUsageStatsProvider);
         mCpuWakeupStats = new CpuWakeupStats(context, R.xml.irq_device_map, mHandler);
         mConfigFile = new AtomicFile(new File(systemDir, "battery_usage_stats_config"));
@@ -506,7 +519,7 @@
 
     public void systemServicesReady() {
         mStats.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider, mPowerStatsStore,
-                Flags.accumulateBatteryUsageStats());
+                isBatteryUsageStatsAccumulationSupported());
 
         MultiStatePowerAttributor attributor = (MultiStatePowerAttributor) mPowerAttributor;
         mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_CPU,
@@ -588,6 +601,12 @@
                 BatteryConsumer.POWER_COMPONENT_CAMERA,
                 Flags.streamlinedMiscBatteryStats());
 
+        // Currently unimplemented.
+        mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_MEMORY,
+                Flags.streamlinedMiscBatteryStats());
+        attributor.setPowerComponentSupported(BatteryConsumer.POWER_COMPONENT_MEMORY,
+                Flags.streamlinedMiscBatteryStats());
+
         // By convention POWER_COMPONENT_ANY represents custom Energy Consumers
         mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_ANY,
                 Flags.streamlinedMiscBatteryStats());
@@ -631,6 +650,13 @@
         registerStatsCallbacks();
     }
 
+    private static boolean isBatteryUsageStatsAccumulationSupported() {
+        return Flags.accumulateBatteryUsageStats()
+                && Flags.streamlinedBatteryStats()
+                && Flags.streamlinedConnectivityBatteryStats()
+                && Flags.streamlinedMiscBatteryStats();
+    }
+
     /**
      * Notifies BatteryStatsService that the system server is ready.
      */
@@ -776,7 +802,8 @@
 
     private void syncStats(String reason, int flags) {
         mStats.collectPowerStatsSamples();
-        awaitUninterruptibly(mWorker.scheduleSync(reason, flags));
+        mWorker.scheduleSync(reason, flags);
+        awaitCompletion();
     }
 
     private void awaitCompletion() {
@@ -1135,7 +1162,7 @@
                             .includeVirtualUids()
                             .setMinConsumedPowerThreshold(minConsumedPowerThreshold);
 
-                    if (Flags.accumulateBatteryUsageStats()) {
+                    if (isBatteryUsageStatsAccumulationSupported()) {
                         query.accumulated();
                     }
 
@@ -3054,7 +3081,7 @@
         if (Flags.streamlinedBatteryStats()) {
             pw.println("  --sample: collect and dump a sample of stats for debugging purpose");
         }
-        if (Flags.accumulateBatteryUsageStats()) {
+        if (isBatteryUsageStatsAccumulationSupported()) {
             pw.println("  --accumulated: continuously accumulated since setup or reset-all");
         }
         pw.println("  <package.name>: optional name of package to filter output by.");
@@ -3670,24 +3697,12 @@
                     android.Manifest.permission.BATTERY_STATS, null);
         }
 
-        Future future;
         if (shouldCollectExternalStats()) {
-            future = mWorker.scheduleSync("get-health-stats-for-uids",
+            mWorker.scheduleSync("get-health-stats-for-uids",
                     BatteryExternalStatsWorker.UPDATE_ALL);
-        } else {
-            future = null;
         }
 
         mHandler.post(() -> {
-            if (future != null) {
-                try {
-                    // Worker uses a separate thread pool, so waiting here won't cause a deadlock
-                    future.get();
-                } catch (InterruptedException | ExecutionException e) {
-                    Slog.e(TAG, "Sync failed", e);
-                }
-            }
-
             final long ident = Binder.clearCallingIdentity();
             int i = -1;
             try {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 37a2fba..78a1fa7 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -3567,8 +3567,10 @@
      * @see AudioManager#addOnDevicesForAttributesChangedListener(
      *      AudioAttributes, Executor, OnDevicesForAttributesChangedListener)
      */
+    @android.annotation.EnforcePermission(anyOf = { MODIFY_AUDIO_ROUTING, QUERY_AUDIO_STATE })
     public void addOnDevicesForAttributesChangedListener(AudioAttributes attributes,
             IDevicesForAttributesCallback callback) {
+        super.addOnDevicesForAttributesChangedListener_enforcePermission();
         mAudioSystem.addOnDevicesForAttributesChangedListener(
                 attributes, false /* forVolume */, callback);
     }
diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java
index dc59e66..7892639 100644
--- a/services/core/java/com/android/server/display/color/ColorDisplayService.java
+++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java
@@ -78,6 +78,7 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.WeaklyReferencedCallback;
 import com.android.internal.util.DumpUtils;
 import com.android.server.DisplayThread;
 import com.android.server.LocalServices;
@@ -1795,6 +1796,7 @@
     /**
      * Interface for applying transforms to a given AppWindow.
      */
+    @WeaklyReferencedCallback
     public interface ColorTransformController {
 
         /**
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index 07343f4..c0aa4cc 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -237,6 +237,11 @@
             Flags::enableHasArrSupport
     );
 
+    private final FlagState mAutoBrightnessModeBedtimeWearFlagState = new FlagState(
+            Flags.FLAG_AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR,
+            Flags::autoBrightnessModeBedtimeWear
+    );
+
     /**
      * @return {@code true} if 'port' is allowed in display layout configuration file.
      */
@@ -503,6 +508,15 @@
     public boolean hasArrSupportFlag() {
         return mHasArrSupport.isEnabled();
     }
+
+    /**
+     * @return {@code true} if bedtime mode specific auto-brightness curve should be loaded and be
+     * applied when bedtime mode is enabled.
+     */
+    public boolean isAutoBrightnessModeBedtimeWearEnabled() {
+        return mAutoBrightnessModeBedtimeWearFlagState.isEnabled();
+    }
+
     /**
      * dumps all flagstates
      * @param pw printWriter
@@ -553,6 +567,7 @@
         pw.println(" " + mBlockAutobrightnessChangesOnStylusUsage);
         pw.println(" " + mIsUserRefreshRateForExternalDisplayEnabled);
         pw.println(" " + mHasArrSupport);
+        pw.println(" " + mAutoBrightnessModeBedtimeWearFlagState);
     }
 
     private static class FlagState {
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index ddb2969..36cadf5 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -422,3 +422,11 @@
     bug: "361433651"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "auto_brightness_mode_bedtime_wear"
+    namespace: "wear_frameworks"
+    description: "Feature flag for loading and applying auto-brightness curve while wear bedtime mode enabled."
+    bug: "350617205"
+    is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 6ac8b22..6c03214 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -5790,6 +5790,9 @@
             }
 
             userInfo.partial = false;
+            if (android.multiuser.Flags.invalidateCacheOnUsersChangedReadOnly()) {
+                UserManager.invalidateCacheOnUserListChange();
+            }
             synchronized (mPackagesLock) {
                 writeUserLP(userData);
             }
@@ -6382,6 +6385,9 @@
                 // on next startup, in case the runtime stops now before stopping and
                 // removing the user completely.
                 userData.info.partial = true;
+                if (android.multiuser.Flags.invalidateCacheOnUsersChangedReadOnly()) {
+                    UserManager.invalidateCacheOnUserListChange();
+                }
                 // Mark it as disabled, so that it isn't returned any more when
                 // profiles are queried.
                 userData.info.flags |= UserInfo.FLAG_DISABLED;
diff --git a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
index 8311034..f90da64 100644
--- a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
+++ b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
@@ -27,12 +27,11 @@
 import android.os.BatteryConsumer;
 import android.os.BatteryStats;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.OutcomeReceiver;
 import android.os.Parcelable;
-import android.os.Process;
 import android.os.SynchronousResultReceiver;
 import android.os.SystemClock;
-import android.os.ThreadLocalWorkSource;
 import android.os.connectivity.WifiActivityEnergyInfo;
 import android.power.PowerStatsInternal;
 import android.telephony.ModemActivityInfo;
@@ -50,11 +49,7 @@
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
-import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
@@ -85,29 +80,23 @@
     // stop running.
     public static final int UID_FINAL_REMOVAL_AFTER_USER_REMOVAL_DELAY_MILLIS = 10_000;
 
-    private final ScheduledExecutorService mExecutorService =
-            Executors.newSingleThreadScheduledExecutor(
-                    (ThreadFactory) r -> {
-                        Thread t = new Thread(
-                                () -> {
-                                    ThreadLocalWorkSource.setUid(Process.myUid());
-                                    r.run();
-                                },
-                                "batterystats-worker");
-                        t.setPriority(Thread.NORM_PRIORITY);
-                        return t;
-                    });
+    // Various types of sync, passed to Handler
+    private static final int SYNC_UPDATE = 1;
+    private static final int SYNC_WAKELOCK_CHANGE = 2;
+    private static final int SYNC_BATTERY_LEVEL_CHANGE = 3;
+    private static final int SYNC_PROCESS_STATE_CHANGE = 4;
+    private static final int SYNC_USER_REMOVAL = 5;
+
+    private final Handler mHandler;
 
     @GuardedBy("mStats")
     private final BatteryStatsImpl mStats;
 
     @GuardedBy("this")
+    @ExternalUpdateFlag
     private int mUpdateFlags = 0;
 
     @GuardedBy("this")
-    private Future<?> mCurrentFuture = null;
-
-    @GuardedBy("this")
     private String mCurrentReason = null;
 
     @GuardedBy("this")
@@ -125,15 +114,6 @@
     @GuardedBy("this")
     private boolean mUseLatestStates = true;
 
-    @GuardedBy("this")
-    private Future<?> mWakelockChangesUpdate;
-
-    @GuardedBy("this")
-    private Future<?> mBatteryLevelSync;
-
-    @GuardedBy("this")
-    private Future<?> mProcessStateSync;
-
     // If both mStats and mWorkerLock need to be synchronized, mWorkerLock must be acquired first.
     private final Object mWorkerLock = new Object();
 
@@ -190,14 +170,15 @@
         }
     }
 
-    public BatteryExternalStatsWorker(Context context, BatteryStatsImpl stats) {
-        this(new Injector(context), stats);
+    public BatteryExternalStatsWorker(Context context, BatteryStatsImpl stats, Handler handler) {
+        this(new Injector(context), stats, handler);
     }
 
     @VisibleForTesting
-    BatteryExternalStatsWorker(Injector injector, BatteryStatsImpl stats) {
+    BatteryExternalStatsWorker(Injector injector, BatteryStatsImpl stats, Handler handler) {
         mInjector = injector;
         mStats = stats;
+        mHandler = handler;
     }
 
     public void systemServicesReady() {
@@ -249,20 +230,20 @@
     }
 
     @Override
-    public synchronized Future<?> scheduleSync(String reason, int flags) {
-        return scheduleSyncLocked(reason, flags);
+    public synchronized void scheduleSync(String reason, @ExternalUpdateFlag int flags) {
+        scheduleSyncLocked(reason, flags);
     }
 
     @Override
-    public synchronized Future<?> scheduleCpuSyncDueToRemovedUid(int uid) {
-        return scheduleSyncLocked("remove-uid", UPDATE_CPU);
+    public synchronized void scheduleCpuSyncDueToRemovedUid(int uid) {
+        scheduleSyncLocked("remove-uid", UPDATE_CPU);
     }
 
     @Override
-    public Future<?> scheduleSyncDueToScreenStateChange(int flags, boolean onBattery,
+    public void scheduleSyncDueToScreenStateChange(@ExternalUpdateFlag int flags, boolean onBattery,
             boolean onBatteryScreenOff, int screenState, int[] perDisplayScreenStates) {
         synchronized (BatteryExternalStatsWorker.this) {
-            if (mCurrentFuture == null || (mUpdateFlags & UPDATE_CPU) == 0) {
+            if (!mHandler.hasMessages(SYNC_UPDATE) || (mUpdateFlags & UPDATE_CPU) == 0) {
                 mOnBattery = onBattery;
                 mOnBatteryScreenOff = onBatteryScreenOff;
                 mUseLatestStates = false;
@@ -270,91 +251,70 @@
             // always update screen state
             mScreenState = screenState;
             mPerDisplayScreenStates = perDisplayScreenStates;
-            return scheduleSyncLocked("screen-state", flags);
+            scheduleSyncLocked("screen-state", flags);
         }
     }
 
     @Override
-    public Future<?> scheduleCpuSyncDueToWakelockChange(long delayMillis) {
+    public void scheduleCpuSyncDueToWakelockChange(long delayMillis) {
         synchronized (BatteryExternalStatsWorker.this) {
-            mWakelockChangesUpdate = scheduleDelayedSyncLocked(mWakelockChangesUpdate,
+            scheduleDelayedSyncLocked(SYNC_WAKELOCK_CHANGE,
                     () -> {
                         scheduleSync("wakelock-change", UPDATE_CPU);
                         scheduleRunnable(() -> mStats.postBatteryNeedsCpuUpdateMsg());
                     },
                     delayMillis);
-            return mWakelockChangesUpdate;
         }
     }
 
     @Override
     public void cancelCpuSyncDueToWakelockChange() {
-        synchronized (BatteryExternalStatsWorker.this) {
-            if (mWakelockChangesUpdate != null) {
-                mWakelockChangesUpdate.cancel(false);
-                mWakelockChangesUpdate = null;
-            }
-        }
+        mHandler.removeMessages(SYNC_WAKELOCK_CHANGE);
     }
 
     @Override
-    public Future<?> scheduleSyncDueToBatteryLevelChange(long delayMillis) {
+    public void scheduleSyncDueToBatteryLevelChange(long delayMillis) {
         synchronized (BatteryExternalStatsWorker.this) {
-            mBatteryLevelSync = scheduleDelayedSyncLocked(mBatteryLevelSync,
+            scheduleDelayedSyncLocked(SYNC_BATTERY_LEVEL_CHANGE,
                     () -> scheduleSync("battery-level", UPDATE_ALL),
                     delayMillis);
-            return mBatteryLevelSync;
         }
     }
 
     @GuardedBy("this")
     private void cancelSyncDueToBatteryLevelChangeLocked() {
-        if (mBatteryLevelSync != null) {
-            mBatteryLevelSync.cancel(false);
-            mBatteryLevelSync = null;
-        }
+        mHandler.removeMessages(SYNC_BATTERY_LEVEL_CHANGE);
     }
 
     @Override
     public void scheduleSyncDueToProcessStateChange(int flags, long delayMillis) {
         synchronized (BatteryExternalStatsWorker.this) {
-            mProcessStateSync = scheduleDelayedSyncLocked(mProcessStateSync,
+            scheduleDelayedSyncLocked(SYNC_PROCESS_STATE_CHANGE,
                     () -> scheduleSync("procstate-change", flags),
                     delayMillis);
         }
     }
 
     public void cancelSyncDueToProcessStateChange() {
-        synchronized (BatteryExternalStatsWorker.this) {
-            if (mProcessStateSync != null) {
-                mProcessStateSync.cancel(false);
-                mProcessStateSync = null;
-            }
-        }
+        mHandler.removeMessages(SYNC_PROCESS_STATE_CHANGE);
     }
 
     @Override
-    public Future<?> scheduleCleanupDueToRemovedUser(int userId) {
-        synchronized (BatteryExternalStatsWorker.this) {
-            try {
-                // Initial quick clean-up after a user removal
-                mExecutorService.schedule(() -> {
-                    synchronized (mStats) {
-                        mStats.clearRemovedUserUidsLocked(userId);
-                    }
-                }, UID_QUICK_REMOVAL_AFTER_USER_REMOVAL_DELAY_MILLIS, TimeUnit.MILLISECONDS);
-
-                // Final clean-up after a user removal, to take care of UIDs that were running
-                // longer than expected
-                return mExecutorService.schedule(() -> {
-                    synchronized (mStats) {
-                        mStats.clearRemovedUserUidsLocked(userId);
-                    }
-                }, UID_FINAL_REMOVAL_AFTER_USER_REMOVAL_DELAY_MILLIS, TimeUnit.MILLISECONDS);
-            } catch (RejectedExecutionException e) {
-                return CompletableFuture.failedFuture(e);
+    public void scheduleCleanupDueToRemovedUser(int userId) {
+        // Initial quick clean-up after a user removal
+        mHandler.postDelayed(() -> {
+            synchronized (mStats) {
+                mStats.clearRemovedUserUidsLocked(userId);
             }
-        }
+        }, SYNC_USER_REMOVAL, UID_QUICK_REMOVAL_AFTER_USER_REMOVAL_DELAY_MILLIS);
+
+        // Final clean-up after a user removal, to take care of UIDs that were running
+        // longer than expected
+        mHandler.postDelayed(() -> {
+            synchronized (mStats) {
+                mStats.clearRemovedUserUidsLocked(userId);
+            }
+        }, SYNC_USER_REMOVAL, UID_FINAL_REMOVAL_AFTER_USER_REMOVAL_DELAY_MILLIS);
     }
 
     /**
@@ -368,42 +328,27 @@
      *         cancel it if needed
      */
     @GuardedBy("this")
-    private Future<?> scheduleDelayedSyncLocked(Future<?> lastScheduledSync, Runnable syncRunnable,
+    private void scheduleDelayedSyncLocked(int what, Runnable syncRunnable,
             long delayMillis) {
-        if (mExecutorService.isShutdown()) {
-            return CompletableFuture.failedFuture(new IllegalStateException("worker shutdown"));
-        }
-
-        if (lastScheduledSync != null) {
+        if (mHandler.hasMessages(what)) {
             // If there's already a scheduled task, leave it as is if we're trying to
             // re-schedule it again with a delay, otherwise cancel and re-schedule it.
             if (delayMillis == 0) {
-                lastScheduledSync.cancel(false);
+                mHandler.removeMessages(what);
             } else {
-                return lastScheduledSync;
+                return;
             }
         }
 
-        try {
-            return mExecutorService.schedule(syncRunnable, delayMillis, TimeUnit.MILLISECONDS);
-        } catch (RejectedExecutionException e) {
-            return CompletableFuture.failedFuture(e);
-        }
+        mHandler.postDelayed(syncRunnable, what, delayMillis);
     }
 
-    public synchronized Future<?> scheduleWrite() {
-        if (mExecutorService.isShutdown()) {
-            return CompletableFuture.failedFuture(new IllegalStateException("worker shutdown"));
-        }
-
+    /**
+     * Schedule and async writing of battery stats to disk
+     */
+    public synchronized void scheduleWrite() {
         scheduleSyncLocked("write", UPDATE_ALL);
-        // Since we use a single threaded executor, we can assume the next scheduled task's
-        // Future finishes after the sync.
-        try {
-            return mExecutorService.submit(mWriteTask);
-        } catch (RejectedExecutionException e) {
-            return CompletableFuture.failedFuture(e);
-        }
+        mHandler.post(mWriteTask);
     }
 
     /**
@@ -411,34 +356,25 @@
      * within the task, never wait on the resulting Future. This will result in a deadlock.
      */
     public synchronized void scheduleRunnable(Runnable runnable) {
-        try {
-            mExecutorService.submit(runnable);
-        } catch (RejectedExecutionException e) {
-            Slog.e(TAG, "Couldn't schedule " + runnable, e);
-        }
+        mHandler.post(runnable);
     }
 
     public void shutdown() {
-        mExecutorService.shutdownNow();
+        mHandler.removeMessages(SYNC_UPDATE);
+        mHandler.removeMessages(SYNC_WAKELOCK_CHANGE);
+        mHandler.removeMessages(SYNC_BATTERY_LEVEL_CHANGE);
+        mHandler.removeMessages(SYNC_PROCESS_STATE_CHANGE);
+        mHandler.removeMessages(SYNC_USER_REMOVAL);
     }
 
     @GuardedBy("this")
-    private Future<?> scheduleSyncLocked(String reason, int flags) {
-        if (mExecutorService.isShutdown()) {
-            return CompletableFuture.failedFuture(new IllegalStateException("worker shutdown"));
-        }
-
-        if (mCurrentFuture == null) {
+    private void scheduleSyncLocked(String reason, @ExternalUpdateFlag int flags) {
+        if (!mHandler.hasMessages(SYNC_UPDATE)) {
             mUpdateFlags = flags;
             mCurrentReason = reason;
-            try {
-                mCurrentFuture = mExecutorService.submit(mSyncTask);
-            } catch (RejectedExecutionException e) {
-                return CompletableFuture.failedFuture(e);
-            }
+            mHandler.postDelayed(mSyncTask, SYNC_UPDATE, 0);
         }
         mUpdateFlags |= flags;
-        return mCurrentFuture;
     }
 
     public long getLastCollectionTimeStamp() {
@@ -468,7 +404,6 @@
                 useLatestStates = mUseLatestStates;
                 mUpdateFlags = 0;
                 mCurrentReason = null;
-                mCurrentFuture = null;
                 mUseLatestStates = true;
                 if ((updateFlags & UPDATE_ALL) == UPDATE_ALL) {
                     cancelSyncDueToBatteryLevelChangeLocked();
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 391071f..c04158f 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -176,7 +176,6 @@
 import java.util.Map;
 import java.util.Queue;
 import java.util.concurrent.Executor;
-import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.locks.ReentrantLock;
@@ -949,19 +948,38 @@
         public @interface ExternalUpdateFlag {
         }
 
-        Future<?> scheduleSync(String reason, int flags);
-        Future<?> scheduleCpuSyncDueToRemovedUid(int uid);
+        /**
+         * Schedules a sync of kernel metrics in accordance with the specified flags.
+         */
+        void scheduleSync(String reason, @ExternalUpdateFlag int flags);
+
+        /**
+         * Schedules a CPU stats sync after a UID removal.
+         */
+        void scheduleCpuSyncDueToRemovedUid(int uid);
 
         /**
          * Schedule a sync because of a screen state change.
          */
-        Future<?> scheduleSyncDueToScreenStateChange(int flags, boolean onBattery,
+        void scheduleSyncDueToScreenStateChange(@ExternalUpdateFlag int flags, boolean onBattery,
                 boolean onBatteryScreenOff, int screenState, int[] perDisplayScreenStates);
-        Future<?> scheduleCpuSyncDueToWakelockChange(long delayMillis);
+
+        /**
+         * Schedules a sync after a wakelock state change
+         */
+        void scheduleCpuSyncDueToWakelockChange(long delayMillis);
+
+        /**
+         * Canceles any pending sync due to a wakelock state change
+         */
         void cancelCpuSyncDueToWakelockChange();
-        Future<?> scheduleSyncDueToBatteryLevelChange(long delayMillis);
+
+        /**
+         * Schedules a sync caused by the battery level change
+         */
+        void scheduleSyncDueToBatteryLevelChange(long delayMillis);
         /** Schedule removal of UIDs corresponding to a removed user */
-        Future<?> scheduleCleanupDueToRemovedUser(int userId);
+        void scheduleCleanupDueToRemovedUser(int userId);
         /** Schedule a sync because of a process state change */
         void scheduleSyncDueToProcessStateChange(int flags, long delayMillis);
     }
@@ -12263,14 +12281,8 @@
             // start time
             long monotonicStartTime =
                     mMonotonicClock.monotonicTime() - batteryUsageStats.getStatsDuration();
-            mHandler.post(() -> {
-                mPowerStatsStore.storeBatteryUsageStats(monotonicStartTime, batteryUsageStats);
-                try {
-                    batteryUsageStats.close();
-                } catch (IOException e) {
-                    Log.e(TAG, "Cannot close BatteryUsageStats", e);
-                }
-            });
+            commitMonotonicClock();
+            mPowerStatsStore.storeBatteryUsageStatsAsync(monotonicStartTime, batteryUsageStats);
         }
     }
 
@@ -15391,6 +15403,10 @@
         mMaxLearnedBatteryCapacityUah = Math.max(mMaxLearnedBatteryCapacityUah, chargeFullUah);
 
         mBatteryTimeToFullSeconds = chargeTimeToFullSeconds;
+
+        if (mAccumulateBatteryUsageStats) {
+            mBatteryUsageStatsProvider.accumulateBatteryUsageStatsAsync(this, mHandler);
+        }
     }
 
     public static boolean isOnBattery(int plugType, int status) {
@@ -17699,6 +17715,13 @@
         }
     }
 
+    /**
+     * Persists the monotonic clock associated with battery stats.
+     */
+    public void commitMonotonicClock() {
+        mMonotonicClock.write();
+    }
+
     @GuardedBy("this")
     public void prepareForDumpLocked() {
         // Need to retrieve current kernel wake lock stats before printing.
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
index b466dd2..265f1dfc 100644
--- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
+++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
@@ -23,17 +23,16 @@
 import android.os.BatteryStats;
 import android.os.BatteryUsageStats;
 import android.os.BatteryUsageStatsQuery;
+import android.os.Handler;
 import android.os.Process;
-import android.os.UidBatteryConsumer;
 import android.util.Log;
-import android.util.Slog;
 import android.util.SparseArray;
 
 import com.android.internal.os.Clock;
 import com.android.internal.os.CpuScalingPolicies;
+import com.android.internal.os.MonotonicClock;
 import com.android.internal.os.PowerProfile;
 
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -49,20 +48,31 @@
     private final PowerStatsStore mPowerStatsStore;
     private final PowerProfile mPowerProfile;
     private final CpuScalingPolicies mCpuScalingPolicies;
+    private final int mAccumulatedBatteryUsageStatsSpanSize;
     private final Clock mClock;
     private final Object mLock = new Object();
     private List<PowerCalculator> mPowerCalculators;
     private UserPowerCalculator mUserPowerCalculator;
+    private long mLastAccumulationMonotonicHistorySize;
+
+    private static class AccumulatedBatteryUsageStats {
+        public BatteryUsageStats.Builder builder;
+        public long startWallClockTime;
+        public long startMonotonicTime;
+        public long endMonotonicTime;
+    }
 
     public BatteryUsageStatsProvider(@NonNull Context context,
             @NonNull PowerAttributor powerAttributor,
             @NonNull PowerProfile powerProfile, @NonNull CpuScalingPolicies cpuScalingPolicies,
-            @NonNull PowerStatsStore powerStatsStore, @NonNull Clock clock) {
+            @NonNull PowerStatsStore powerStatsStore, int accumulatedBatteryUsageStatsSpanSize,
+            @NonNull Clock clock) {
         mContext = context;
         mPowerAttributor = powerAttributor;
         mPowerStatsStore = powerStatsStore;
         mPowerProfile = powerProfile;
         mCpuScalingPolicies = cpuScalingPolicies;
+        mAccumulatedBatteryUsageStatsSpanSize = accumulatedBatteryUsageStatsSpanSize;
         mClock = clock;
         mUserPowerCalculator = new UserPowerCalculator();
 
@@ -85,7 +95,10 @@
                     mPowerCalculators.add(
                             new CpuPowerCalculator(mCpuScalingPolicies, mPowerProfile));
                 }
-                mPowerCalculators.add(new MemoryPowerCalculator(mPowerProfile));
+                if (!mPowerAttributor.isPowerComponentSupported(
+                        BatteryConsumer.POWER_COMPONENT_MEMORY)) {
+                    mPowerCalculators.add(new MemoryPowerCalculator(mPowerProfile));
+                }
                 if (!mPowerAttributor.isPowerComponentSupported(
                         BatteryConsumer.POWER_COMPONENT_WAKELOCK)) {
                     mPowerCalculators.add(new WakelockPowerCalculator(mPowerProfile));
@@ -141,7 +154,11 @@
                         BatteryConsumer.POWER_COMPONENT_AMBIENT_DISPLAY)) {
                     mPowerCalculators.add(new AmbientDisplayPowerCalculator(mPowerProfile));
                 }
-                mPowerCalculators.add(new IdlePowerCalculator(mPowerProfile));
+                // IDLE power attribution is covered by WakelockPowerStatsProcessor
+                if (!mPowerAttributor.isPowerComponentSupported(
+                        BatteryConsumer.POWER_COMPONENT_WAKELOCK)) {
+                    mPowerCalculators.add(new IdlePowerCalculator(mPowerProfile));
+                }
                 if (!mPowerAttributor.isPowerComponentSupported(
                         BatteryConsumer.POWER_COMPONENT_ANY)) {
                     mPowerCalculators.add(new CustomEnergyConsumerPowerCalculator(mPowerProfile));
@@ -159,53 +176,45 @@
     }
 
     /**
-     * Compute BatteryUsageStats for the period since the last accumulated stats were stored,
-     * add them to the accumulated stats and save the result.
+     * Conditionally runs a battery usage stats accumulation on the supplied handler.
+     */
+    public void accumulateBatteryUsageStatsAsync(BatteryStatsImpl stats, Handler handler) {
+        synchronized (this) {
+            long historySize = stats.getHistory().getMonotonicHistorySize();
+            if (historySize - mLastAccumulationMonotonicHistorySize
+                    < mAccumulatedBatteryUsageStatsSpanSize) {
+                return;
+            }
+            mLastAccumulationMonotonicHistorySize = historySize;
+        }
+
+        handler.post(() -> accumulateBatteryUsageStats(stats));
+    }
+
+    /**
+     * Computes BatteryUsageStats for the period since the last accumulated stats were stored,
+     * adds them to the accumulated stats and saves the result.
      */
     public void accumulateBatteryUsageStats(BatteryStatsImpl stats) {
-        BatteryUsageStats.Builder accumulatedBatteryUsageStatsBuilder = null;
+        AccumulatedBatteryUsageStats accumulatedStats = loadAccumulatedBatteryUsageStats();
 
-        PowerStatsSpan powerStatsSpan = mPowerStatsStore.loadPowerStatsSpan(
-                AccumulatedBatteryUsageStatsSection.ID,
-                AccumulatedBatteryUsageStatsSection.TYPE);
-        if (powerStatsSpan != null) {
-            List<PowerStatsSpan.Section> sections = powerStatsSpan.getSections();
-            for (int i = sections.size() - 1; i >= 0; i--) {
-                PowerStatsSpan.Section section = sections.get(i);
-                if (AccumulatedBatteryUsageStatsSection.TYPE.equals(section.getType())) {
-                    accumulatedBatteryUsageStatsBuilder =
-                            ((AccumulatedBatteryUsageStatsSection) section)
-                                    .getBatteryUsageStatsBuilder();
-                    break;
-                }
-            }
-        }
+        final BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder()
+                .setMaxStatsAgeMs(0)
+                .includeProcessStateData()
+                .includePowerStateData()
+                .includeScreenStateData()
+                .build();
+        updateAccumulatedBatteryUsageStats(accumulatedStats, stats, query);
 
-        // TODO(b/366493365): add the current batteryusagestats directly into the "accumulated"
-        // builder to avoid allocating a second CursorWindow
-        BatteryUsageStats.Builder currentBatteryUsageStatsBuilder =
-                getCurrentBatteryUsageStatsBuilder(stats,
-                        new BatteryUsageStatsQuery.Builder()
-                                .setMaxStatsAgeMs(0)
-                                .includeProcessStateData()
-                                .includePowerStateData()
-                                .includeScreenStateData()
-                                .build(),
-                        mClock.currentTimeMillis());
-
-        if (accumulatedBatteryUsageStatsBuilder == null) {
-            accumulatedBatteryUsageStatsBuilder = currentBatteryUsageStatsBuilder;
-        } else {
-            accumulatedBatteryUsageStatsBuilder.add(currentBatteryUsageStatsBuilder.build());
-            currentBatteryUsageStatsBuilder.discard();
-        }
-
-        powerStatsSpan = new PowerStatsSpan(AccumulatedBatteryUsageStatsSection.ID);
+        PowerStatsSpan powerStatsSpan = new PowerStatsSpan(AccumulatedBatteryUsageStatsSection.ID);
         powerStatsSpan.addSection(
-                new AccumulatedBatteryUsageStatsSection(accumulatedBatteryUsageStatsBuilder));
-
+                new AccumulatedBatteryUsageStatsSection(accumulatedStats.builder));
+        powerStatsSpan.addTimeFrame(accumulatedStats.startMonotonicTime,
+                accumulatedStats.startWallClockTime,
+                accumulatedStats.endMonotonicTime - accumulatedStats.startMonotonicTime);
+        stats.commitMonotonicClock();
         mPowerStatsStore.storePowerStatsSpanAsync(powerStatsSpan,
-                accumulatedBatteryUsageStatsBuilder::discard);
+                accumulatedStats.builder::discard);
     }
 
     /**
@@ -252,68 +261,73 @@
             BatteryUsageStatsQuery query, long currentTimeMs) {
         if ((query.getFlags()
                 & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_ACCUMULATED) != 0) {
-            return getAccumulatedBatteryUsageStats(stats, query);
-        } else if (query.getToTimestamp() == 0) {
-            return getCurrentBatteryUsageStats(stats, query, currentTimeMs);
+            return getAccumulatedBatteryUsageStats(stats, query, currentTimeMs);
+        } else if (query.getAggregatedToTimestamp() == 0) {
+            BatteryUsageStats.Builder builder = computeBatteryUsageStats(stats, query,
+                    query.getMonotonicStartTime(),
+                    query.getMonotonicEndTime(), currentTimeMs);
+            return builder.build();
         } else {
             return getAggregatedBatteryUsageStats(stats, query);
         }
     }
 
     private BatteryUsageStats getAccumulatedBatteryUsageStats(BatteryStatsImpl stats,
-            BatteryUsageStatsQuery query) {
+            BatteryUsageStatsQuery query, long currentTimeMs) {
+        AccumulatedBatteryUsageStats accumulatedStats = loadAccumulatedBatteryUsageStats();
+        updateAccumulatedBatteryUsageStats(accumulatedStats, stats, query);
+        return accumulatedStats.builder.build();
+    }
+
+    private AccumulatedBatteryUsageStats loadAccumulatedBatteryUsageStats() {
+        AccumulatedBatteryUsageStats stats = new AccumulatedBatteryUsageStats();
+        stats.startWallClockTime = 0;
+        stats.startMonotonicTime = MonotonicClock.UNDEFINED;
+        stats.endMonotonicTime = MonotonicClock.UNDEFINED;
         PowerStatsSpan powerStatsSpan = mPowerStatsStore.loadPowerStatsSpan(
                 AccumulatedBatteryUsageStatsSection.ID,
                 AccumulatedBatteryUsageStatsSection.TYPE);
-
-        BatteryUsageStats.Builder accumulatedBatteryUsageStatsBuilder = null;
         if (powerStatsSpan != null) {
             List<PowerStatsSpan.Section> sections = powerStatsSpan.getSections();
-            if (sections.size() == 1) {
-                accumulatedBatteryUsageStatsBuilder =
-                        ((AccumulatedBatteryUsageStatsSection) sections.get(0))
-                                .getBatteryUsageStatsBuilder();
-            } else {
-                Slog.wtf(TAG, "Unexpected number of sections for type "
-                        + AccumulatedBatteryUsageStatsSection.TYPE);
+            for (int i = sections.size() - 1; i >= 0; i--) {
+                PowerStatsSpan.Section section = sections.get(i);
+                if (AccumulatedBatteryUsageStatsSection.TYPE.equals(section.getType())) {
+                    stats.builder = ((AccumulatedBatteryUsageStatsSection) section)
+                            .getBatteryUsageStatsBuilder();
+                    stats.startWallClockTime = powerStatsSpan.getMetadata().getStartTime();
+                    stats.startMonotonicTime = powerStatsSpan.getMetadata().getStartMonotonicTime();
+                    stats.endMonotonicTime = powerStatsSpan.getMetadata().getEndMonotonicTime();
+                    break;
+                }
             }
         }
+        return stats;
+    }
 
-        BatteryUsageStats currentBatteryUsageStats = getCurrentBatteryUsageStats(stats, query,
+    private void updateAccumulatedBatteryUsageStats(AccumulatedBatteryUsageStats accumulatedStats,
+            BatteryStatsImpl stats, BatteryUsageStatsQuery query) {
+        // TODO(b/366493365): add the current batteryusagestats directly into
+        //  `accumulatedStats.builder` to avoid allocating a second CursorWindow
+        BatteryUsageStats.Builder remainingBatteryUsageStats = computeBatteryUsageStats(stats,
+                query, accumulatedStats.endMonotonicTime, query.getMonotonicEndTime(),
                 mClock.currentTimeMillis());
 
-        BatteryUsageStats result;
-        if (accumulatedBatteryUsageStatsBuilder == null) {
-            result = currentBatteryUsageStats;
+        if (accumulatedStats.builder == null) {
+            accumulatedStats.builder = remainingBatteryUsageStats;
+            accumulatedStats.startWallClockTime = stats.getStartClockTime();
+            accumulatedStats.startMonotonicTime = stats.getMonotonicStartTime();
+            accumulatedStats.endMonotonicTime = accumulatedStats.startMonotonicTime
+                    + accumulatedStats.builder.getStatsDuration();
         } else {
-            accumulatedBatteryUsageStatsBuilder.add(currentBatteryUsageStats);
-            try {
-                currentBatteryUsageStats.close();
-            } catch (IOException ex) {
-                Slog.e(TAG, "Closing BatteryUsageStats", ex);
-            }
-            result = accumulatedBatteryUsageStatsBuilder.build();
+            accumulatedStats.builder.add(remainingBatteryUsageStats.build());
+            accumulatedStats.endMonotonicTime += remainingBatteryUsageStats.getStatsDuration();
+            remainingBatteryUsageStats.discard();
         }
-
-        return result;
     }
 
-    private BatteryUsageStats getCurrentBatteryUsageStats(BatteryStatsImpl stats,
-            BatteryUsageStatsQuery query, long currentTimeMs) {
-        BatteryUsageStats.Builder builder = getCurrentBatteryUsageStatsBuilder(stats, query,
-                currentTimeMs);
-        BatteryUsageStats batteryUsageStats = builder.build();
-        if (batteryUsageStats.isProcessStateDataIncluded()) {
-            verify(batteryUsageStats);
-        }
-        return batteryUsageStats;
-    }
-
-    private BatteryUsageStats.Builder getCurrentBatteryUsageStatsBuilder(BatteryStatsImpl stats,
-            BatteryUsageStatsQuery query, long currentTimeMs) {
-        final long realtimeUs = mClock.elapsedRealtime() * 1000;
-        final long uptimeUs = mClock.uptimeMillis() * 1000;
-
+    private BatteryUsageStats.Builder computeBatteryUsageStats(BatteryStatsImpl stats,
+            BatteryUsageStatsQuery query, long monotonicStartTime, long monotonicEndTime,
+            long currentTimeMs) {
         final boolean includePowerModels = (query.getFlags()
                 & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_MODELS) != 0;
         final boolean includeProcessStateData = ((query.getFlags()
@@ -324,11 +338,8 @@
         final double minConsumedPowerThreshold = query.getMinConsumedPowerThreshold();
 
         String[] customEnergyConsumerNames;
-        long monotonicStartTime, monotonicEndTime;
         synchronized (stats) {
             customEnergyConsumerNames = stats.getCustomEnergyConsumerNames();
-            monotonicStartTime = stats.getMonotonicStartTime();
-            monotonicEndTime = stats.getMonotonicEndTime();
         }
 
         final BatteryUsageStats.Builder batteryUsageStatsBuilder = new BatteryUsageStats.Builder(
@@ -337,12 +348,31 @@
                 query.isPowerStateDataNeeded(), minConsumedPowerThreshold);
 
         synchronized (stats) {
-            // TODO(b/188068523): use a monotonic clock to ensure resilience of order and duration
-            // of batteryUsageStats sessions to wall-clock adjustments
-            batteryUsageStatsBuilder.setStatsStartTimestamp(stats.getStartClockTime());
-            batteryUsageStatsBuilder.setStatsEndTimestamp(currentTimeMs);
             final List<PowerCalculator> powerCalculators = getPowerCalculators();
             if (!powerCalculators.isEmpty()) {
+                if (monotonicStartTime != MonotonicClock.UNDEFINED
+                        || monotonicEndTime != MonotonicClock.UNDEFINED) {
+                    throw new IllegalStateException("BatteryUsageStatsQuery specifies a time "
+                            + "range that is incompatible with PowerCalculators: "
+                            + powerCalculators);
+                }
+            }
+
+            if (monotonicStartTime == MonotonicClock.UNDEFINED) {
+                monotonicStartTime = stats.getMonotonicStartTime();
+            }
+            batteryUsageStatsBuilder.setStatsStartTimestamp(stats.getStartClockTime()
+                    + (monotonicStartTime - stats.getMonotonicStartTime()));
+            if (monotonicEndTime != MonotonicClock.UNDEFINED) {
+                batteryUsageStatsBuilder.setStatsEndTimestamp(stats.getStartClockTime()
+                        + (monotonicEndTime - stats.getMonotonicStartTime()));
+            } else {
+                batteryUsageStatsBuilder.setStatsEndTimestamp(currentTimeMs);
+            }
+
+            if (!powerCalculators.isEmpty()) {
+                final long realtimeUs = mClock.elapsedRealtime() * 1000;
+                final long uptimeUs = mClock.uptimeMillis() * 1000;
                 final int[] powerComponents = query.getPowerComponents();
                 SparseArray<? extends BatteryStats.Uid> uidStats = stats.getUidStats();
                 for (int i = uidStats.size() - 1; i >= 0; i--) {
@@ -381,8 +411,7 @@
                 monotonicStartTime, monotonicEndTime);
 
         // Combine apps by the user if necessary
-        mUserPowerCalculator.calculate(batteryUsageStatsBuilder, stats, realtimeUs, uptimeUs,
-                query);
+        mUserPowerCalculator.calculate(batteryUsageStatsBuilder, stats, 0, 0, query);
 
         populateGeneralInfo(batteryUsageStatsBuilder, stats);
         return batteryUsageStatsBuilder;
@@ -402,48 +431,6 @@
         }
     }
 
-    // STOPSHIP(b/229906525): remove verification before shipping
-    private static boolean sErrorReported;
-
-    private void verify(BatteryUsageStats stats) {
-        if (sErrorReported) {
-            return;
-        }
-
-        final double precision = 2.0;   // Allow rounding errors up to 2 mAh
-        final int[] components =
-                {BatteryConsumer.POWER_COMPONENT_CPU,
-                        BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
-                        BatteryConsumer.POWER_COMPONENT_WIFI,
-                        BatteryConsumer.POWER_COMPONENT_BLUETOOTH};
-        final int[] states =
-                {BatteryConsumer.PROCESS_STATE_FOREGROUND,
-                        BatteryConsumer.PROCESS_STATE_BACKGROUND,
-                        BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE,
-                        BatteryConsumer.PROCESS_STATE_CACHED};
-        for (UidBatteryConsumer ubc : stats.getUidBatteryConsumers()) {
-            for (int component : components) {
-                double consumedPower = ubc.getConsumedPower(ubc.getKey(component));
-                double sumStates = 0;
-                for (int state : states) {
-                    sumStates += ubc.getConsumedPower(ubc.getKey(component, state));
-                }
-                if (sumStates > consumedPower + precision) {
-                    String error = "Sum of states exceeds total. UID = " + ubc.getUid() + " "
-                            + BatteryConsumer.powerComponentIdToString(component)
-                            + " total = " + consumedPower + " states = " + sumStates;
-                    if (!sErrorReported) {
-                        Slog.wtf(TAG, error);
-                        sErrorReported = true;
-                    } else {
-                        Slog.e(TAG, error);
-                    }
-                    return;
-                }
-            }
-        }
-    }
-
     private BatteryUsageStats getAggregatedBatteryUsageStats(BatteryStatsImpl stats,
             BatteryUsageStatsQuery query) {
         final boolean includePowerModels = (query.getFlags()
@@ -489,8 +476,10 @@
             // Per BatteryUsageStatsQuery API, the "from" timestamp is *exclusive*,
             // while the "to" timestamp is *inclusive*.
             boolean isInRange =
-                    (query.getFromTimestamp() == 0 || minTime > query.getFromTimestamp())
-                            && (query.getToTimestamp() == 0 || maxTime <= query.getToTimestamp());
+                    (query.getAggregatedFromTimestamp() == 0
+                            || minTime > query.getAggregatedFromTimestamp())
+                            && (query.getAggregatedToTimestamp() == 0
+                            || maxTime <= query.getAggregatedToTimestamp());
             if (!isInRange) {
                 continue;
             }
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsSpan.java b/services/core/java/com/android/server/power/stats/PowerStatsSpan.java
index fc0611f..5105272 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsSpan.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsSpan.java
@@ -25,6 +25,7 @@
 import android.util.TimeUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.MonotonicClock;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 
@@ -147,6 +148,40 @@
             mTimeFrames.add(timeFrame);
         }
 
+        long getStartTime() {
+            long startTime = Long.MAX_VALUE;
+            for (int i = 0; i < mTimeFrames.size(); i++) {
+                TimeFrame timeFrame = mTimeFrames.get(i);
+                if (timeFrame.startTime < startTime) {
+                    startTime = timeFrame.startTime;
+                }
+            }
+            return startTime != Long.MAX_VALUE ? startTime : 0;
+        }
+
+        long getStartMonotonicTime() {
+            long startTime = Long.MAX_VALUE;
+            for (int i = 0; i < mTimeFrames.size(); i++) {
+                TimeFrame timeFrame = mTimeFrames.get(i);
+                if (timeFrame.startMonotonicTime < startTime) {
+                    startTime = timeFrame.startMonotonicTime;
+                }
+            }
+            return startTime != Long.MAX_VALUE ? startTime : MonotonicClock.UNDEFINED;
+        }
+
+        long getEndMonotonicTime() {
+            long maxTime = Long.MIN_VALUE;
+            for (int i = 0; i < mTimeFrames.size(); i++) {
+                TimeFrame timeFrame = mTimeFrames.get(i);
+                long endTime = timeFrame.startMonotonicTime + timeFrame.duration;
+                if (endTime > maxTime) {
+                    maxTime = endTime;
+                }
+            }
+            return maxTime != Long.MIN_VALUE ? maxTime : MonotonicClock.UNDEFINED;
+        }
+
         void addSection(String sectionType) {
             // The number of sections per span is small, so there is no need to use a Set
             if (!mSections.contains(sectionType)) {
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsStore.java b/services/core/java/com/android/server/power/stats/PowerStatsStore.java
index 5a6f973..3673617 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsStore.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsStore.java
@@ -149,7 +149,6 @@
      * Saves the specified span in the store.
      */
     public void storePowerStatsSpan(PowerStatsSpan span) {
-        maybeClearLegacyStore();
         lockStoreDirectory();
         try {
             if (!mStoreDir.exists()) {
@@ -203,13 +202,23 @@
      * Stores a {@link PowerStatsSpan} containing a single section for the supplied
      * battery usage stats.
      */
-    public void storeBatteryUsageStats(long monotonicStartTime,
+    public void storeBatteryUsageStatsAsync(long monotonicStartTime,
             BatteryUsageStats batteryUsageStats) {
-        PowerStatsSpan span = new PowerStatsSpan(monotonicStartTime);
-        span.addTimeFrame(monotonicStartTime, batteryUsageStats.getStatsStartTimestamp(),
-                batteryUsageStats.getStatsDuration());
-        span.addSection(new BatteryUsageStatsSection(batteryUsageStats));
-        storePowerStatsSpan(span);
+        mHandler.post(() -> {
+            try {
+                PowerStatsSpan span = new PowerStatsSpan(monotonicStartTime);
+                span.addTimeFrame(monotonicStartTime, batteryUsageStats.getStatsStartTimestamp(),
+                        batteryUsageStats.getStatsDuration());
+                span.addSection(new BatteryUsageStatsSection(batteryUsageStats));
+                storePowerStatsSpan(span);
+            } finally {
+                try {
+                    batteryUsageStats.close();
+                } catch (IOException e) {
+                    Slog.e(TAG, "Cannot close BatteryUsageStats", e);
+                }
+            }
+        });
     }
 
     /**
diff --git a/services/core/java/com/android/server/power/stats/format/BinaryStatePowerStatsLayout.java b/services/core/java/com/android/server/power/stats/format/BinaryStatePowerStatsLayout.java
index 4a26d83..657701b 100644
--- a/services/core/java/com/android/server/power/stats/format/BinaryStatePowerStatsLayout.java
+++ b/services/core/java/com/android/server/power/stats/format/BinaryStatePowerStatsLayout.java
@@ -21,7 +21,9 @@
 public class BinaryStatePowerStatsLayout extends EnergyConsumerPowerStatsLayout {
     public BinaryStatePowerStatsLayout() {
         addDeviceSectionUsageDuration();
+        addDeviceSectionPowerEstimate();
         addUidSectionUsageDuration();
+        addUidSectionPowerEstimate();
     }
 
     public BinaryStatePowerStatsLayout(PowerStats.Descriptor descriptor) {
diff --git a/services/core/java/com/android/server/power/stats/processor/PowerStatsExporter.java b/services/core/java/com/android/server/power/stats/processor/PowerStatsExporter.java
index c7ad564..c8170a1 100644
--- a/services/core/java/com/android/server/power/stats/processor/PowerStatsExporter.java
+++ b/services/core/java/com/android/server/power/stats/processor/PowerStatsExporter.java
@@ -117,6 +117,7 @@
                     PowerStatsSpan.Section section = sections.get(k);
                     populateBatteryUsageStatsBuilder(batteryUsageStatsBuilder,
                             ((AggregatedPowerStatsSection) section).getAggregatedPowerStats());
+                    // TODO(b/371614748): close the builder
                 }
             }
 
@@ -197,6 +198,7 @@
                 && powerState == BatteryConsumer.POWER_STATE_BATTERY;
 
         double[] totalPower = new double[1];
+        long[] durationMs = new long[1];
         MultiStateStats.States.forEachTrackedStateCombination(
                 powerComponentStats.getConfig().getDeviceStateConfig(),
                 states -> {
@@ -209,6 +211,7 @@
                     }
 
                     totalPower[0] += layout.getDevicePowerEstimate(deviceStats);
+                    durationMs[0] += layout.getUsageDuration(deviceStats);
 
                     if (hasBatteryLevelProperties) {
                         gatherBatteryLevelInfo(batteryLevelInfo, deviceStats);
@@ -223,9 +226,13 @@
         if (key != null) {
             deviceScope.addConsumedPower(key, totalPower[0],
                     BatteryConsumer.POWER_MODEL_UNDEFINED);
+            deviceScope.addUsageDurationMillis(key, durationMs[0]);
         }
-        deviceScope.addConsumedPower(powerComponentId, totalPower[0],
-                BatteryConsumer.POWER_MODEL_UNDEFINED);
+        key = deviceScope.getKey(powerComponentId, BatteryConsumer.PROCESS_STATE_UNSPECIFIED);
+        if (key != null) {
+            deviceScope.addConsumedPower(key, totalPower[0], BatteryConsumer.POWER_MODEL_UNDEFINED);
+            deviceScope.addUsageDurationMillis(key, durationMs[0]);
+        }
     }
 
     private void gatherBatteryLevelInfo(BatteryLevelInfo batteryLevelInfo, long[] deviceStats) {
@@ -373,9 +380,15 @@
 
             if (resultScreenState != BatteryConsumer.SCREEN_STATE_UNSPECIFIED
                     || resultPowerState != BatteryConsumer.POWER_STATE_UNSPECIFIED) {
-                builder.addConsumedPower(powerComponentId,
-                        powerByProcState[BatteryConsumer.PROCESS_STATE_UNSPECIFIED],
-                        BatteryConsumer.POWER_MODEL_UNDEFINED);
+                BatteryConsumer.Key key = builder.getKey(powerComponentId,
+                        BatteryConsumer.PROCESS_STATE_UNSPECIFIED);
+                if (key != null) {
+                    builder.addConsumedPower(key,
+                            powerByProcState[BatteryConsumer.PROCESS_STATE_UNSPECIFIED],
+                            BatteryConsumer.POWER_MODEL_UNDEFINED);
+                    builder.addUsageDurationMillis(key,
+                            durationByProcState[BatteryConsumer.PROCESS_STATE_UNSPECIFIED]);
+                }
             }
             powerAllApps += powerByProcState[BatteryConsumer.PROCESS_STATE_UNSPECIFIED];
         }
diff --git a/services/core/java/com/android/server/vcn/VcnContext.java b/services/core/java/com/android/server/vcn/VcnContext.java
index 6a4c9c2..a492a72 100644
--- a/services/core/java/com/android/server/vcn/VcnContext.java
+++ b/services/core/java/com/android/server/vcn/VcnContext.java
@@ -70,10 +70,6 @@
         return mIsInTestMode;
     }
 
-    public boolean isFlagNetworkMetricMonitorEnabled() {
-        return mFeatureFlags.networkMetricMonitor();
-    }
-
     public boolean isFlagIpSecTransformStateEnabled() {
         // TODO: b/328844044: Ideally this code should gate the behavior by checking the
         // android.net.platform.flags.ipsec_transform_state flag but that flag is not accessible
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index b574782..a81ad22 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -1913,7 +1913,6 @@
                 mIpSecManager.applyTunnelModeTransform(tunnelIface, direction, transform);
 
                 if (direction == IpSecManager.DIRECTION_IN
-                        && mVcnContext.isFlagNetworkMetricMonitorEnabled()
                         && mVcnContext.isFlagIpSecTransformStateEnabled()) {
                     mUnderlyingNetworkController.updateInboundTransform(mUnderlying, transform);
                 }
diff --git a/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java b/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java
index b9b1060..0d4c373 100644
--- a/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java
+++ b/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java
@@ -62,12 +62,6 @@
             @Nullable PersistableBundleWrapper carrierConfig,
             @NonNull NetworkMetricMonitorCallback callback)
             throws IllegalAccessException {
-        if (!vcnContext.isFlagNetworkMetricMonitorEnabled()) {
-            // Caller error
-            logWtf("networkMetricMonitor flag disabled");
-            throw new IllegalAccessException("networkMetricMonitor flag disabled");
-        }
-
         mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext");
         mNetwork = Objects.requireNonNull(network, "Missing network");
         mCallback = Objects.requireNonNull(callback, "Missing callback");
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
index 2b0ca08..ad5bc72 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
@@ -204,8 +204,7 @@
         List<NetworkCallback> oldCellCallbacks = new ArrayList<>(mCellBringupCallbacks);
         mCellBringupCallbacks.clear();
 
-        if (mVcnContext.isFlagNetworkMetricMonitorEnabled()
-                && mVcnContext.isFlagIpSecTransformStateEnabled()) {
+        if (mVcnContext.isFlagIpSecTransformStateEnabled()) {
             for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) {
                 evaluator.close();
             }
@@ -431,8 +430,7 @@
                 .getAllSubIdsInGroup(mSubscriptionGroup)
                 .equals(newSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))) {
 
-            if (mVcnContext.isFlagNetworkMetricMonitorEnabled()
-                    && mVcnContext.isFlagIpSecTransformStateEnabled()) {
+            if (mVcnContext.isFlagIpSecTransformStateEnabled()) {
                 reevaluateNetworks();
             }
             return;
@@ -447,8 +445,7 @@
      */
     public void updateInboundTransform(
             @NonNull UnderlyingNetworkRecord currentNetwork, @NonNull IpSecTransform transform) {
-        if (!mVcnContext.isFlagNetworkMetricMonitorEnabled()
-                || !mVcnContext.isFlagIpSecTransformStateEnabled()) {
+        if (!mVcnContext.isFlagIpSecTransformStateEnabled()) {
             logWtf("#updateInboundTransform: unexpected call; flags missing");
             return;
         }
@@ -575,8 +572,7 @@
 
         @Override
         public void onLost(@NonNull Network network) {
-            if (mVcnContext.isFlagNetworkMetricMonitorEnabled()
-                    && mVcnContext.isFlagIpSecTransformStateEnabled()) {
+            if (mVcnContext.isFlagIpSecTransformStateEnabled()) {
                 mUnderlyingNetworkRecords.get(network).close();
             }
 
@@ -652,8 +648,7 @@
     class NetworkEvaluatorCallbackImpl implements NetworkEvaluatorCallback {
         @Override
         public void onEvaluationResultChanged() {
-            if (!mVcnContext.isFlagNetworkMetricMonitorEnabled()
-                    || !mVcnContext.isFlagIpSecTransformStateEnabled()) {
+            if (!mVcnContext.isFlagIpSecTransformStateEnabled()) {
                 logWtf("#onEvaluationResultChanged: unexpected call; flags missing");
                 return;
             }
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
index c852fb4..53b0751 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
@@ -193,8 +193,7 @@
     }
 
     private static boolean isIpSecPacketLossDetectorEnabled(VcnContext vcnContext) {
-        return vcnContext.isFlagIpSecTransformStateEnabled()
-                && vcnContext.isFlagNetworkMetricMonitorEnabled();
+        return vcnContext.isFlagIpSecTransformStateEnabled();
     }
 
     /** Get the comparator for UnderlyingNetworkEvaluator */
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 460de01..054f931 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -5508,7 +5508,8 @@
                 clearAllDrawn();
                 // Reset the draw state in order to prevent the starting window to be immediately
                 // dismissed when the app still has the surface.
-                if (!isVisible() && !isClientVisible()) {
+                if (!Flags.resetDrawStateOnClientInvisible()
+                        && !isVisible() && !isClientVisible()) {
                     forAllWindows(w -> {
                         if (w.mWinAnimator.mDrawState == HAS_DRAWN) {
                             w.mWinAnimator.resetDrawState();
@@ -6852,7 +6853,7 @@
         } else if (associatedTask.getActivity(
                 r -> r.isVisibleRequested() && !r.firstWindowDrawn) == null) {
             // The last drawn activity may not be the one that owns the starting window.
-            final ActivityRecord r = associatedTask.topActivityContainsStartingWindow();
+            final ActivityRecord r = associatedTask.getActivity(ar -> ar.mStartingData != null);
             if (r != null) {
                 r.removeStartingWindow();
             }
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 70f9ebb..ccd5996 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -201,6 +201,9 @@
             infoBuilder.setTouchableRegion(window.getFrame());
             infoBuilder.setAppProgressAllowed((window.getAttrs().privateFlags
                     & PRIVATE_FLAG_APP_PROGRESS_GENERATION_ALLOWED) != 0);
+            if (currentTask != null) {
+                infoBuilder.setFocusedTaskId(currentTask.mTaskId);
+            }
             mNavigationMonitor.startMonitor(window, navigationObserver);
 
             ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "startBackNavigation currentTask=%s, "
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 76f2437..6141876 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -3304,6 +3304,16 @@
                 android.os.Process.killProcess(mSession.mPid);
             }
         }
+
+        // Because the client is notified to be invisible, it should no longer be considered as
+        // drawn state. This prevent the app from showing incomplete content if the app is
+        // requested to be visible in a short time (e.g. before activity stopped).
+        if (Flags.resetDrawStateOnClientInvisible() && !clientVisible && mActivityRecord != null
+                && mWinAnimator.mDrawState == HAS_DRAWN) {
+            mWinAnimator.resetDrawState();
+            // Make sure the app can report drawn if it becomes visible again.
+            forceReportingResized();
+        }
     }
 
     void onStartFreezingScreen() {
diff --git a/services/core/xsd/vts/Android.bp b/services/core/xsd/vts/Android.bp
index 4d3c79e..e1478d6 100644
--- a/services/core/xsd/vts/Android.bp
+++ b/services/core/xsd/vts/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_android_kernel",
     // See: http://go/android-license-faq
     // A large-scale-change added 'default_applicable_licenses' to import
     // all of the 'license_kinds' from "frameworks_base_license"
diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig
index 0990691..e2ac22d 100644
--- a/services/java/com/android/server/flags.aconfig
+++ b/services/java/com/android/server/flags.aconfig
@@ -10,6 +10,14 @@
 }
 
 flag {
+    name: "remove_java_service_manager_cache"
+    namespace: "system_performance"
+    description: "This flag turns off Java's Service Manager caching mechanism."
+    bug: "333854840"
+    is_fixed_read_only: true
+}
+
+flag {
      name: "remove_text_service"
      namespace: "wear_frameworks"
      description: "Remove TextServiceManagerService on Wear"
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/OWNERS b/services/tests/mockingservicestests/src/com/android/server/alarm/OWNERS
index 6f207fb1..6eb986b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/OWNERS
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/OWNERS
@@ -1 +1 @@
-include /apex/jobscheduler/OWNERS
+include /apex/jobscheduler/ALARM_OWNERS
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/OWNERS b/services/tests/mockingservicestests/src/com/android/server/job/OWNERS
index 6f207fb1..c8345f7 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/OWNERS
+++ b/services/tests/mockingservicestests/src/com/android/server/job/OWNERS
@@ -1 +1 @@
-include /apex/jobscheduler/OWNERS
+include /apex/jobscheduler/JOB_OWNERS
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
index 7b635d4..d7b60cf 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
@@ -39,7 +39,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.connectivity.WifiActivityEnergyInfo;
-import android.platform.test.ravenwood.RavenwoodRule;
+import android.platform.test.ravenwood.RavenwoodConfig;
 import android.power.PowerStatsInternal;
 import android.util.IntArray;
 import android.util.SparseArray;
@@ -52,7 +52,6 @@
 import com.android.internal.os.PowerProfile;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 
 import java.util.Arrays;
@@ -67,25 +66,27 @@
 @SuppressWarnings("GuardedBy")
 @android.platform.test.annotations.DisabledOnRavenwood
 public class BatteryExternalStatsWorkerTest {
-    @Rule
-    public final RavenwoodRule mRavenwood = new RavenwoodRule();
+    @RavenwoodConfig.Config
+    public final RavenwoodConfig mRavenwood = new RavenwoodConfig.Builder().build();
 
     private BatteryExternalStatsWorker mBatteryExternalStatsWorker;
     private TestPowerStatsInternal mPowerStatsInternal;
+    private Handler mHandler;
 
     @Before
     public void setUp() {
         final Context context = InstrumentationRegistry.getContext();
 
+        mHandler = new Handler(Looper.getMainLooper());
         BatteryStatsImpl batteryStats = new BatteryStatsImpl(
                 new BatteryStatsImpl.BatteryStatsConfig.Builder().build(), Clock.SYSTEM_CLOCK,
                 new MonotonicClock(0, Clock.SYSTEM_CLOCK), null,
-                new Handler(Looper.getMainLooper()), null, null, null,
+                mHandler, null, null, null,
                 new PowerProfile(context, true /* forTest */), buildScalingPolicies(),
                 new PowerStatsUidResolver());
         mPowerStatsInternal = new TestPowerStatsInternal();
         mBatteryExternalStatsWorker =
-                new BatteryExternalStatsWorker(new TestInjector(context), batteryStats);
+                new BatteryExternalStatsWorker(new TestInjector(context), batteryStats, mHandler);
     }
 
     @Test
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
index d36b553..d6f5036 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
@@ -38,7 +38,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Random;
-import java.util.concurrent.Future;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -324,9 +323,8 @@
         private boolean mSyncScheduled;
 
         @Override
-        public Future<?> scheduleCpuSyncDueToWakelockChange(long delayMillis) {
+        public void scheduleCpuSyncDueToWakelockChange(long delayMillis) {
             mSyncScheduled = true;
-            return null;
         }
 
         public boolean isSyncScheduled() {
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
index e40a3e3..b67ec8b 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
@@ -653,6 +653,44 @@
     }
 
     @Test
+    public void getMonotonicHistorySize() {
+        long lastHistorySize = mHistory.getMonotonicHistorySize();
+        mHistory.forceRecordAllHistory();
+
+        mClock.realtime = 1000;
+        mClock.uptime = 1000;
+        mHistory.recordEvent(mClock.realtime, mClock.uptime,
+                BatteryStats.HistoryItem.EVENT_JOB_START, "job", 42);
+        long size = mHistory.getMonotonicHistorySize();
+        assertThat(size).isGreaterThan(lastHistorySize);
+        lastHistorySize = size;
+
+        mHistory.startNextFile(mClock.realtime);
+
+        size = mHistory.getMonotonicHistorySize();
+        assertThat(size).isEqualTo(lastHistorySize);
+
+        mClock.realtime = 2000;
+        mClock.uptime = 2000;
+        mHistory.recordEvent(mClock.realtime, mClock.uptime,
+                BatteryStats.HistoryItem.EVENT_JOB_FINISH, "job", 42);
+
+        size = mHistory.getMonotonicHistorySize();
+        assertThat(size).isGreaterThan(lastHistorySize);
+        lastHistorySize = size;
+
+        mHistory.startNextFile(mClock.realtime);
+
+        mClock.realtime = 3000;
+        mClock.uptime = 3000;
+        mHistory.recordEvent(mClock.realtime, mClock.uptime,
+                HistoryItem.EVENT_ALARM, "alarm", 42);
+
+        size = mHistory.getMonotonicHistorySize();
+        assertThat(size).isGreaterThan(lastHistorySize);
+    }
+
+    @Test
     public void testVarintParceler() {
         long[] values = {
                 0,
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
index 2408cc1..177f30a 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
@@ -159,7 +159,7 @@
         }
         mPowerStatsStore = new PowerStatsStore(systemDir, mHandler);
         mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context, mPowerAttributor,
-                mPowerProfile, mBatteryStatsImpl.getCpuScalingPolicies(), mPowerStatsStore,
+                mPowerProfile, mBatteryStatsImpl.getCpuScalingPolicies(), mPowerStatsStore, 0,
                 mMockClock);
     }
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java
index 3931201..5912a99 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java
@@ -134,7 +134,7 @@
 
     private int getNumberOfUidsInBatteryStats() throws Exception {
         ArraySet<Integer> uids = new ArraySet<>();
-        final String dumpsys = executeShellCommand("dumpsys batterystats --checkin");
+        final String dumpsys = executeShellCommand("dumpsys batterystats -c");
         for (String line : dumpsys.split("\n")) {
             final String[] parts = line.trim().split(",");
             if (parts.length < 5 ||
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
index b30224b..e9d95fc 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
@@ -22,6 +22,7 @@
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -119,7 +120,7 @@
                 .isWithin(PRECISION).of(0.4);
 
         assertThat(batteryUsageStats.getStatsStartTimestamp()).isEqualTo(12345);
-        assertThat(batteryUsageStats.getStatsEndTimestamp()).isEqualTo(54321);
+        assertThat(batteryUsageStats.getStatsEndTimestamp()).isEqualTo(180 * MINUTE_IN_MS);
     }
 
     @Test
@@ -142,7 +143,7 @@
 
         BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
                 mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
-                mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), mMockClock);
+                mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), 0, mMockClock);
 
         final BatteryUsageStats batteryUsageStats =
                 provider.getBatteryUsageStats(batteryStats,
@@ -249,7 +250,8 @@
             }
         }
 
-        mStatsRule.setCurrentTime(54321);
+        setTime(180 * MINUTE_IN_MS);
+
         return batteryStats;
     }
 
@@ -266,7 +268,7 @@
 
         BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
                 powerAttributor, mStatsRule.getPowerProfile(),
-                mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), mMockClock);
+                mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), 0, mMockClock);
 
         return provider.getBatteryUsageStats(batteryStats, BatteryUsageStatsQuery.DEFAULT);
     }
@@ -296,7 +298,7 @@
 
         BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
                 mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
-                mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), mMockClock);
+                mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), 0, mMockClock);
 
         final BatteryUsageStats batteryUsageStats =
                 provider.getBatteryUsageStats(batteryStats,
@@ -385,7 +387,7 @@
 
         BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
                 mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
-                mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), mMockClock);
+                mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), 0, mMockClock);
 
         final BatteryUsageStats batteryUsageStats =
                 provider.getBatteryUsageStats(batteryStats,
@@ -474,7 +476,7 @@
 
         BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
                 mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
-                mStatsRule.getCpuScalingPolicies(), powerStatsStore, mMockClock);
+                mStatsRule.getCpuScalingPolicies(), powerStatsStore, 0, mMockClock);
 
         batteryStats.saveBatteryUsageStatsOnReset(provider, powerStatsStore,
                 /* accumulateBatteryUsageStats */ false);
@@ -564,8 +566,19 @@
     }
 
     @Test
-    public void accumulateBatteryUsageStats() {
+    public void accumulateBatteryUsageStats() throws Throwable {
+        accumulateBatteryUsageStats(10000000, 1);
+        // Accumulate every 200 bytes of battery history
+        accumulateBatteryUsageStats(200, 2);
+        accumulateBatteryUsageStats(50, 5);
+        // Accumulate on every invocation of accumulateBatteryUsageStats
+        accumulateBatteryUsageStats(0, 7);
+    }
+
+    private void accumulateBatteryUsageStats(int accumulatedBatteryUsageStatsSpanSize,
+            int expectedNumberOfUpdates) throws Throwable {
         BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
+        batteryStats.forceRecordAllHistory();
 
         setTime(5 * MINUTE_IN_MS);
 
@@ -574,69 +587,86 @@
             batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
         }
 
-        PowerStatsStore powerStatsStore = new PowerStatsStore(
+        PowerStatsStore powerStatsStore = spy(new PowerStatsStore(
                 new File(mStatsRule.getHistoryDir(), getClass().getSimpleName()),
-                mStatsRule.getHandler());
+                mStatsRule.getHandler()));
         powerStatsStore.reset();
 
-        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
-                mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
-                mStatsRule.getCpuScalingPolicies(), powerStatsStore, mMockClock);
+        int[] count = new int[1];
+        doAnswer(inv -> {
+            count[0]++;
+            return null;
+        }).when(powerStatsStore).storePowerStatsSpan(any(PowerStatsSpan.class));
 
-        batteryStats.saveBatteryUsageStatsOnReset(provider, powerStatsStore,
-                /* accumulateBatteryUsageStats */ true);
+        MultiStatePowerAttributor powerAttributor = new MultiStatePowerAttributor(mContext,
+                powerStatsStore, mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(),
+                () -> 3500, new PowerStatsUidResolver());
+        for (int powerComponentId = 0; powerComponentId < BatteryConsumer.POWER_COMPONENT_COUNT;
+                powerComponentId++) {
+            powerAttributor.setPowerComponentSupported(powerComponentId, true);
+        }
+        powerAttributor.setPowerComponentSupported(BatteryConsumer.POWER_COMPONENT_ANY, true);
+
+        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
+                powerAttributor, mStatsRule.getPowerProfile(),
+                mStatsRule.getCpuScalingPolicies(), powerStatsStore,
+                accumulatedBatteryUsageStatsSpanSize, mMockClock);
+
+        provider.accumulateBatteryUsageStatsAsync(batteryStats, mStatsRule.getHandler());
 
         synchronized (batteryStats) {
             batteryStats.noteFlashlightOnLocked(APP_UID,
                     10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
         }
+
+        provider.accumulateBatteryUsageStatsAsync(batteryStats, mStatsRule.getHandler());
+
         synchronized (batteryStats) {
             batteryStats.noteFlashlightOffLocked(APP_UID,
                     20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
         }
 
-        synchronized (batteryStats) {
-            batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
-        }
+        provider.accumulateBatteryUsageStatsAsync(batteryStats, mStatsRule.getHandler());
 
         synchronized (batteryStats) {
             batteryStats.noteFlashlightOnLocked(APP_UID,
                     30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS);
         }
+
+        provider.accumulateBatteryUsageStatsAsync(batteryStats, mStatsRule.getHandler());
+
         synchronized (batteryStats) {
             batteryStats.noteFlashlightOffLocked(APP_UID,
                     50 * MINUTE_IN_MS, 50 * MINUTE_IN_MS);
         }
         setTime(55 * MINUTE_IN_MS);
-        synchronized (batteryStats) {
-            batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
-        }
+
+        provider.accumulateBatteryUsageStatsAsync(batteryStats, mStatsRule.getHandler());
 
         // This section has not been saved yet, but should be added to the accumulated totals
         synchronized (batteryStats) {
             batteryStats.noteFlashlightOnLocked(APP_UID,
                     80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
         }
+
+        provider.accumulateBatteryUsageStatsAsync(batteryStats, mStatsRule.getHandler());
+
         synchronized (batteryStats) {
             batteryStats.noteFlashlightOffLocked(APP_UID,
                     110 * MINUTE_IN_MS, 110 * MINUTE_IN_MS);
         }
         setTime(115 * MINUTE_IN_MS);
 
-        // Await completion
-        ConditionVariable done = new ConditionVariable();
-        mStatsRule.getHandler().post(done::open);
-        done.block();
+        // Pick up the remainder of battery history that has not yet been accumulated
+        provider.accumulateBatteryUsageStats(batteryStats);
+
+        mStatsRule.waitForBackgroundThread();
 
         BatteryUsageStats stats = provider.getBatteryUsageStats(batteryStats,
                 new BatteryUsageStatsQuery.Builder().accumulated().build());
-
         assertThat(stats.getStatsStartTimestamp()).isEqualTo(5 * MINUTE_IN_MS);
         assertThat(stats.getStatsEndTimestamp()).isEqualTo(115 * MINUTE_IN_MS);
 
-        // Section 1 (saved): 20 - 10 = 10
-        // Section 2 (saved): 50 - 30 = 20
-        // Section 3 (fresh): 110 - 80 = 30
         // Total: 10 + 20 + 30 = 60
         assertThat(stats.getAggregateBatteryConsumer(
                         BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
@@ -657,6 +687,8 @@
         assertThat(uidBatteryConsumer
                 .getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
                 .isEqualTo(60 * MINUTE_IN_MS);
+
+        assertThat(count[0]).isEqualTo(expectedNumberOfUpdates);
     }
 
     private void setTime(long timeMs) {
@@ -682,7 +714,7 @@
 
         BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
                 mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
-                mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), mMockClock);
+                mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), 0, mMockClock);
 
         PowerStatsStore powerStatsStore = mock(PowerStatsStore.class);
         doAnswer(invocation -> {
@@ -702,7 +734,7 @@
             assertThat(uid.getConsumedPower(componentId1))
                     .isWithin(PRECISION).of(8.33333);
             return null;
-        }).when(powerStatsStore).storeBatteryUsageStats(anyLong(), any());
+        }).when(powerStatsStore).storeBatteryUsageStatsAsync(anyLong(), any());
 
         mStatsRule.getBatteryStats().saveBatteryUsageStatsOnReset(provider, powerStatsStore,
                 /* accumulateBatteryUsageStats */ false);
@@ -714,7 +746,7 @@
 
         mStatsRule.waitForBackgroundThread();
 
-        verify(powerStatsStore).storeBatteryUsageStats(anyLong(), any());
+        verify(powerStatsStore).storeBatteryUsageStatsAsync(anyLong(), any());
     }
 
     @Test
@@ -746,7 +778,7 @@
 
         BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
                 mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
-                mStatsRule.getCpuScalingPolicies(), powerStatsStore, mMockClock);
+                mStatsRule.getCpuScalingPolicies(), powerStatsStore, 0, mMockClock);
 
         BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder()
                 .aggregateSnapshots(0, 3000)
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
index 2c03f9d..1e4454c 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
@@ -43,7 +43,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Queue;
-import java.util.concurrent.Future;
 
 /**
  * Mocks a BatteryStatsImpl object.
@@ -288,30 +287,25 @@
         public int flags = 0;
 
         @Override
-        public Future<?> scheduleSync(String reason, int flags) {
-            return null;
+        public void scheduleSync(String reason, int flags) {
         }
 
         @Override
-        public Future<?> scheduleCleanupDueToRemovedUser(int userId) {
-            return null;
+        public void scheduleCleanupDueToRemovedUser(int userId) {
         }
 
         @Override
-        public Future<?> scheduleCpuSyncDueToRemovedUid(int uid) {
-            return null;
+        public void scheduleCpuSyncDueToRemovedUid(int uid) {
         }
 
         @Override
-        public Future<?> scheduleSyncDueToScreenStateChange(int flag, boolean onBattery,
+        public void scheduleSyncDueToScreenStateChange(int flag, boolean onBattery,
                 boolean onBatteryScreenOff, int screenState, int[] perDisplayScreenStates) {
             flags |= flag;
-            return null;
         }
 
         @Override
-        public Future<?> scheduleCpuSyncDueToWakelockChange(long delayMillis) {
-            return null;
+        public void scheduleCpuSyncDueToWakelockChange(long delayMillis) {
         }
 
         @Override
@@ -319,8 +313,7 @@
         }
 
         @Override
-        public Future<?> scheduleSyncDueToBatteryLevelChange(long delayMillis) {
-            return null;
+        public void scheduleSyncDueToBatteryLevelChange(long delayMillis) {
         }
 
         @Override
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerStatsCollectorTest.java
index 03491bc..0d5d277 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerStatsCollectorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerStatsCollectorTest.java
@@ -65,12 +65,14 @@
     private WakelockPowerStatsLayout mStatsLayout = new WakelockPowerStatsLayout();
 
     @Before
-    public void setup() {
-        mBatteryStats = new MockBatteryStatsImpl(mClock);
+    public void setup() throws Throwable {
+        mBatteryStats = mStatsRule.getBatteryStats();
         mBatteryStats.setPowerStatsCollectorEnabled(POWER_COMPONENT_WAKELOCK, true);
         mBatteryStats.getPowerStatsCollector(POWER_COMPONENT_WAKELOCK)
                 .addConsumer(ps -> mPowerStats = ps);
         mBatteryStats.onSystemReady(mock(Context.class));
+        // onSystemReady schedules the initial power stats collection. Wait for it to finish
+        mStatsRule.waitForBackgroundThread();
     }
 
     @Test
@@ -79,9 +81,6 @@
         PowerStatsCollector powerStatsCollector = mBatteryStats.getPowerStatsCollector(
                 POWER_COMPONENT_WAKELOCK);
 
-        // Establish a baseline
-        powerStatsCollector.collectAndDeliverStats();
-
         mBatteryStats.forceRecordAllHistory();
 
         mStatsRule.advanceSuspendedTime(1000);
diff --git a/services/tests/servicestests/src/com/android/server/OWNERS b/services/tests/servicestests/src/com/android/server/OWNERS
index d49bc43..d8a9400 100644
--- a/services/tests/servicestests/src/com/android/server/OWNERS
+++ b/services/tests/servicestests/src/com/android/server/OWNERS
@@ -1,4 +1,4 @@
-per-file *Alarm* = file:/apex/jobscheduler/OWNERS
+per-file *Alarm* = file:/apex/jobscheduler/ALARM_OWNERS
 per-file *AppOp* = file:/core/java/android/permission/OWNERS
 per-file *BinaryTransparency* = file:/core/java/android/transparency/OWNERS
 per-file *Bluetooth* = file:platform/packages/modules/Bluetooth:master:/framework/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/am/BatteryStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/am/BatteryStatsServiceTest.java
index 467c15d..ea70287 100644
--- a/services/tests/servicestests/src/com/android/server/am/BatteryStatsServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/BatteryStatsServiceTest.java
@@ -55,6 +55,7 @@
         File systemDir = context.getCacheDir();
         Handler handler = new Handler(mBgThread.getLooper());
         mBatteryStatsService = new BatteryStatsService(context, systemDir);
+        mBatteryStatsService.setRailsStatsCollectionEnabled(false);
     }
 
     @After
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 577b02a..c30b4bb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3214,23 +3214,32 @@
         assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
     }
 
+    @SetupWindows(addWindows = W_ACTIVITY)
     @Test
     public void testSetVisibility_visibleToInvisible() {
-        final ActivityRecord activity = new ActivityBuilder(mAtm)
-                .setCreateTask(true).build();
+        final TestTransitionPlayer player = registerTestTransitionPlayer();
+        final ActivityRecord activity = mAppWindow.mActivityRecord;
+        makeWindowVisibleAndDrawn(mAppWindow);
         // By default, activity is visible.
         assertTrue(activity.isVisible());
         assertTrue(activity.isVisibleRequested());
-        assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
+        assertTrue(mAppWindow.isDrawn());
+        assertFalse(mAppWindow.setReportResizeHints());
 
         // Request the activity to be invisible. Since the visibility changes, app transition
         // animation should be applied on this activity.
-        mDisplayContent.prepareAppTransition(0);
+        activity.mTransitionController.requestCloseTransitionIfNeeded(activity);
         activity.setVisibility(false);
         assertTrue(activity.isVisible());
         assertFalse(activity.isVisibleRequested());
-        assertFalse(activity.mDisplayContent.mOpeningApps.contains(activity));
-        assertTrue(activity.mDisplayContent.mClosingApps.contains(activity));
+
+        player.start();
+        mSetFlagsRule.enableFlags(Flags.FLAG_RESET_DRAW_STATE_ON_CLIENT_INVISIBLE);
+        // ActivityRecord#commitVisibility(false) -> WindowState#sendAppVisibilityToClients().
+        player.finish();
+        assertFalse(activity.isVisible());
+        assertFalse("Reset draw state after committing invisible", mAppWindow.isDrawn());
+        assertTrue("Set pending redraw hint", mAppWindow.setReportResizeHints());
     }
 
     @Test
diff --git a/tests/JobSchedulerPerfTests/OWNERS b/tests/JobSchedulerPerfTests/OWNERS
index 6f207fb1..c8345f7 100644
--- a/tests/JobSchedulerPerfTests/OWNERS
+++ b/tests/JobSchedulerPerfTests/OWNERS
@@ -1 +1 @@
-include /apex/jobscheduler/OWNERS
+include /apex/jobscheduler/JOB_OWNERS
diff --git a/tests/JobSchedulerTestApp/OWNERS b/tests/JobSchedulerTestApp/OWNERS
index 6f207fb1..c8345f7 100644
--- a/tests/JobSchedulerTestApp/OWNERS
+++ b/tests/JobSchedulerTestApp/OWNERS
@@ -1 +1 @@
-include /apex/jobscheduler/OWNERS
+include /apex/jobscheduler/JOB_OWNERS
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index e29e462..e045f10 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -224,7 +224,6 @@
         doReturn(mFeatureFlags).when(mVcnContext).getFeatureFlags();
         doReturn(true).when(mVcnContext).isFlagSafeModeTimeoutConfigEnabled();
         doReturn(true).when(mVcnContext).isFlagIpSecTransformStateEnabled();
-        doReturn(true).when(mVcnContext).isFlagNetworkMetricMonitorEnabled();
 
         doReturn(mUnderlyingNetworkController)
                 .when(mDeps)
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
index 421e1ad..bc7ff47 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
@@ -127,7 +127,6 @@
                                 false /* isInTestMode */));
         doNothing().when(mVcnContext).ensureRunningOnLooperThread();
 
-        doReturn(true).when(mVcnContext).isFlagNetworkMetricMonitorEnabled();
         doReturn(true).when(mVcnContext).isFlagIpSecTransformStateEnabled();
 
         setupSystemService(
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
index 588624b..6f31d8d 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
@@ -226,7 +226,6 @@
     private void resetVcnContext(VcnContext vcnContext) {
         reset(vcnContext);
         doNothing().when(vcnContext).ensureRunningOnLooperThread();
-        doReturn(true).when(vcnContext).isFlagNetworkMetricMonitorEnabled();
         doReturn(true).when(vcnContext).isFlagIpSecTransformStateEnabled();
     }