Merge "[Fill Dialog Improvements] Implement Fill Dialog improvements" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index 48ee065..6367002 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -27268,11 +27268,11 @@
     method public void addActiveProcessingPictureListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.quality.MediaQualityManager.ActiveProcessingPictureListener);
     method public void createPictureProfile(@NonNull android.media.quality.PictureProfile);
     method public void createSoundProfile(@NonNull android.media.quality.SoundProfile);
-    method @NonNull public java.util.List<android.media.quality.PictureProfile> getAvailablePictureProfiles();
-    method @NonNull public java.util.List<android.media.quality.SoundProfile> getAvailableSoundProfiles();
+    method @NonNull public java.util.List<android.media.quality.PictureProfile> getAvailablePictureProfiles(boolean);
+    method @NonNull public java.util.List<android.media.quality.SoundProfile> getAvailableSoundProfiles(boolean);
     method @NonNull public java.util.List<android.media.quality.ParamCapability> getParamCapabilities(@NonNull java.util.List<java.lang.String>);
-    method @Nullable public android.media.quality.PictureProfile getPictureProfile(int, @NonNull String);
-    method @Nullable public android.media.quality.SoundProfile getSoundProfile(int, @NonNull String);
+    method @Nullable public android.media.quality.PictureProfile getPictureProfile(int, @NonNull String, boolean);
+    method @Nullable public android.media.quality.SoundProfile getSoundProfile(int, @NonNull String, boolean);
     method public boolean isAmbientBacklightEnabled();
     method public boolean isAutoPictureQualityEnabled();
     method public boolean isAutoSoundQualityEnabled();
@@ -27303,7 +27303,7 @@
 
   public abstract static class MediaQualityManager.PictureProfileCallback {
     ctor public MediaQualityManager.PictureProfileCallback();
-    method public void onError(int);
+    method public void onError(@Nullable String, int);
     method public void onParamCapabilitiesChanged(@Nullable String, @NonNull java.util.List<android.media.quality.ParamCapability>);
     method public void onPictureProfileAdded(@NonNull String, @NonNull android.media.quality.PictureProfile);
     method public void onPictureProfileRemoved(@NonNull String, @NonNull android.media.quality.PictureProfile);
@@ -27312,7 +27312,7 @@
 
   public abstract static class MediaQualityManager.SoundProfileCallback {
     ctor public MediaQualityManager.SoundProfileCallback();
-    method public void onError(int);
+    method public void onError(@Nullable String, int);
     method public void onParamCapabilitiesChanged(@Nullable String, @NonNull java.util.List<android.media.quality.ParamCapability>);
     method public void onSoundProfileAdded(@NonNull String, @NonNull android.media.quality.SoundProfile);
     method public void onSoundProfileRemoved(@NonNull String, @NonNull android.media.quality.SoundProfile);
@@ -34765,7 +34765,10 @@
     method public android.os.MessageQueue getMessageQueue();
     method public boolean hasMessages(android.os.Handler, Object, int);
     method public boolean hasMessages(android.os.Handler, Object, Runnable);
+    method @FlaggedApi("android.os.message_queue_testability") public boolean isBlockedOnSyncBarrier();
     method public android.os.Message next();
+    method @FlaggedApi("android.os.message_queue_testability") @Nullable public Long peekWhen();
+    method @FlaggedApi("android.os.message_queue_testability") @Nullable public android.os.Message pop();
     method public void recycle(android.os.Message);
     method public void release();
   }
@@ -47460,6 +47463,7 @@
     field public static final long NETWORK_TYPE_BITMASK_IWLAN = 131072L; // 0x20000L
     field public static final long NETWORK_TYPE_BITMASK_LTE = 4096L; // 0x1000L
     field @Deprecated public static final long NETWORK_TYPE_BITMASK_LTE_CA = 262144L; // 0x40000L
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final long NETWORK_TYPE_BITMASK_NB_IOT_NTN = 1048576L; // 0x100000L
     field public static final long NETWORK_TYPE_BITMASK_NR = 524288L; // 0x80000L
     field public static final long NETWORK_TYPE_BITMASK_TD_SCDMA = 65536L; // 0x10000L
     field public static final long NETWORK_TYPE_BITMASK_UMTS = 4L; // 0x4L
@@ -47479,6 +47483,7 @@
     field @Deprecated public static final int NETWORK_TYPE_IDEN = 11; // 0xb
     field public static final int NETWORK_TYPE_IWLAN = 18; // 0x12
     field public static final int NETWORK_TYPE_LTE = 13; // 0xd
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int NETWORK_TYPE_NB_IOT_NTN = 21; // 0x15
     field public static final int NETWORK_TYPE_NR = 20; // 0x14
     field public static final int NETWORK_TYPE_TD_SCDMA = 17; // 0x11
     field public static final int NETWORK_TYPE_UMTS = 3; // 0x3
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 831e005..4a4776d 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -7990,10 +7990,10 @@
     method public void addGlobalActiveProcessingPictureListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.quality.MediaQualityManager.ActiveProcessingPictureListener);
     method @NonNull public java.util.List<java.lang.String> getPictureProfileAllowList();
     method @NonNull public java.util.List<java.lang.String> getPictureProfilePackageNames();
-    method @NonNull public java.util.List<android.media.quality.PictureProfile> getPictureProfilesByPackage(@NonNull String);
+    method @NonNull public java.util.List<android.media.quality.PictureProfile> getPictureProfilesByPackage(@NonNull String, boolean);
     method @NonNull public java.util.List<java.lang.String> getSoundProfileAllowList();
     method @NonNull public java.util.List<java.lang.String> getSoundProfilePackageNames();
-    method @NonNull public java.util.List<android.media.quality.SoundProfile> getSoundProfilesByPackage(@NonNull String);
+    method @NonNull public java.util.List<android.media.quality.SoundProfile> getSoundProfilesByPackage(@NonNull String, boolean);
     method public void setAutoPictureQualityEnabled(boolean);
     method public void setAutoSoundQualityEnabled(boolean);
     method public boolean setDefaultPictureProfile(@Nullable String);
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 38aea64..03ef669 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -1279,7 +1279,24 @@
      *
      * <p>This method should be utilized when an activity wants to nudge the user to switch
      * to the web application in cases where the web may provide the user with a better
-     * experience. Note that this method does not guarantee that the education will be shown.</p>
+     * experience. Note that this method does not guarantee that the education will be shown.
+     *
+     * <p>The number of times that the "Open in browser" education can be triggered by this method
+     * is limited per application, and, when shown, the education appears above the app's content.
+     * For these reasons, developers should use this method sparingly when it is least
+     * disruptive to the user to show the education and when it is optimal to switch the user to a
+     * browser session. Before requesting to show the education, developers should assert that they
+     * have set a link that can be used by the "Open in browser" feature through either
+     * {@link AssistContent#EXTRA_AUTHENTICATING_USER_WEB_URI} or
+     * {@link AssistContent#setWebUri} so that users are navigated to a relevant page if they choose
+     * to switch to the browser. If a URI is not set using either method, "Open in browser" will
+     * utilize a generic link if available which will direct users to the homepage of the site
+     * associated with the app. The generic link is provided for a limited number of applications by
+     * the system and cannot be edited by developers. If none of these options contains a valid URI,
+     * the user will not be provided with the option to switch to the browser and the education will
+     * not be shown if requested.
+     *
+     * @see android.app.assist.AssistContent#EXTRA_SESSION_TRANSFER_WEB_URI
      */
     @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION)
     public final void requestOpenInBrowserEducation() {
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 33ba058..69d3e8d 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -1189,6 +1189,18 @@
         return procState == PROCESS_STATE_FOREGROUND_SERVICE;
     }
 
+    /** @hide Should this process state be considered jank perceptible? */
+    public static final boolean isProcStateJankPerceptible(int procState) {
+        if (Flags.jankPerceptibleNarrow()) {
+            return procState == PROCESS_STATE_PERSISTENT_UI
+                || procState == PROCESS_STATE_TOP
+                || procState == PROCESS_STATE_IMPORTANT_FOREGROUND
+                || procState == PROCESS_STATE_TOP_SLEEPING;
+        } else {
+            return !isProcStateCached(procState);
+        }
+    }
+
     /** @hide requestType for assist context: only basic information. */
     public static final int ASSIST_CONTEXT_BASIC = 0;
 
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index eccb6ff..abdfb535 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -44,6 +44,8 @@
 import android.os.PowerExemptionManager.TempAllowListType;
 import android.os.TransactionTooLargeException;
 import android.os.WorkSource;
+import android.os.instrumentation.IOffsetCallback;
+import android.os.instrumentation.MethodDescriptor;
 import android.util.ArraySet;
 import android.util.Pair;
 
@@ -1352,6 +1354,14 @@
             String reason, int exitInfoReason);
 
     /**
+     * Queries the offset data for a given method on a process.
+     * @hide
+     */
+    public abstract void getExecutableMethodFileOffsets(@NonNull String processName,
+            int pid, int uid, @NonNull MethodDescriptor methodDescriptor,
+            @NonNull IOffsetCallback callback);
+
+    /**
      * Add a creator token for all embedded intents (stored as extra) of the given intent.
      *
      * @param intent The given intent
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 27661ce..48b74f2 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -165,6 +165,10 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.os.instrumentation.ExecutableMethodFileOffsets;
+import android.os.instrumentation.IOffsetCallback;
+import android.os.instrumentation.MethodDescriptor;
+import android.os.instrumentation.MethodDescriptorParser;
 import android.permission.IPermissionManager;
 import android.provider.BlockedNumberContract;
 import android.provider.CalendarContract;
@@ -2236,6 +2240,29 @@
             args.arg6 = uiTranslationSpec;
             sendMessage(H.UPDATE_UI_TRANSLATION_STATE, args);
         }
+
+        @Override
+        public void getExecutableMethodFileOffsets(
+                @NonNull MethodDescriptor methodDescriptor,
+                @NonNull IOffsetCallback resultCallback) {
+            Method method = MethodDescriptorParser.parseMethodDescriptor(
+                    getClass().getClassLoader(), methodDescriptor);
+            VMDebug.ExecutableMethodFileOffsets location =
+                    VMDebug.getExecutableMethodFileOffsets(method);
+            try {
+                if (location == null) {
+                    resultCallback.onResult(null);
+                    return;
+                }
+                ExecutableMethodFileOffsets ret = new ExecutableMethodFileOffsets();
+                ret.containerPath = location.getContainerPath();
+                ret.containerOffset = location.getContainerOffset();
+                ret.methodOffset = location.getMethodOffset();
+                resultCallback.onResult(ret);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
     }
 
     private @NonNull SafeCancellationTransport createSafeCancellationTransport(
@@ -3918,12 +3945,7 @@
             if (mLastProcessState == processState) {
                 return;
             }
-            // Do not issue a transitional GC if we are transitioning between 2 cached states.
-            // Only update if the state flips between cached and uncached or vice versa
-            if (ActivityManager.isProcStateCached(mLastProcessState)
-                    != ActivityManager.isProcStateCached(processState)) {
-                updateVmProcessState(processState);
-            }
+            updateVmProcessState(mLastProcessState, processState);
             mLastProcessState = processState;
             if (localLOGV) {
                 Slog.i(TAG, "******************* PROCESS STATE CHANGED TO: " + processState
@@ -3932,18 +3954,21 @@
         }
     }
 
+    /** Converts a process state to a VM process state. */
+    private static int toVmProcessState(int processState) {
+        final int state = ActivityManager.isProcStateJankPerceptible(processState)
+                ? VM_PROCESS_STATE_JANK_PERCEPTIBLE
+                : VM_PROCESS_STATE_JANK_IMPERCEPTIBLE;
+        return state;
+    }
+
     /** Update VM state based on ActivityManager.PROCESS_STATE_* constants. */
-    // Currently ART VM only uses state updates for Transitional GC, and thus
-    // this function initiates a Transitional GC for transitions into Cached apps states.
-    private void updateVmProcessState(int processState) {
-        // Only a transition into Cached state should result in a Transitional GC request
-        // to the ART runtime. Update VM state to JANK_IMPERCEPTIBLE in that case.
-        // Note that there are 4 possible cached states currently, all of which are
-        // JANK_IMPERCEPTIBLE from GC point of view.
-        final int state = ActivityManager.isProcStateCached(processState)
-                ? VM_PROCESS_STATE_JANK_IMPERCEPTIBLE
-                : VM_PROCESS_STATE_JANK_PERCEPTIBLE;
-        VMRuntime.getRuntime().updateProcessState(state);
+    private void updateVmProcessState(int lastProcessState, int newProcessState) {
+        final int state = toVmProcessState(newProcessState);
+        if (lastProcessState == PROCESS_STATE_UNKNOWN
+                || state != toVmProcessState(lastProcessState)) {
+            VMRuntime.getRuntime().updateProcessState(state);
+        }
     }
 
     @Override
diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl
index 06d01ec..063501b 100644
--- a/core/java/android/app/IApplicationThread.aidl
+++ b/core/java/android/app/IApplicationThread.aidl
@@ -46,6 +46,8 @@
 import android.os.PersistableBundle;
 import android.os.RemoteCallback;
 import android.os.SharedMemory;
+import android.os.instrumentation.IOffsetCallback;
+import android.os.instrumentation.MethodDescriptor;
 import android.view.autofill.AutofillId;
 import android.view.translation.TranslationSpec;
 import android.view.translation.UiTranslationSpec;
@@ -183,4 +185,6 @@
     void scheduleTimeoutService(IBinder token, int startId);
     void scheduleTimeoutServiceForType(IBinder token, int startId, int fgsType);
     void schedulePing(in RemoteCallback pong);
+    void getExecutableMethodFileOffsets(in MethodDescriptor methodDescriptor,
+            in IOffsetCallback resultCallback);
 }
diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index bea3010..720e045 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -156,3 +156,10 @@
      bug: "362537357"
      is_exported: true
 }
+
+flag {
+    name: "jank_perceptible_narrow"
+    namespace: "system_performance"
+    description: "Narrow the scope of Jank Perceptible"
+    bug: "304837972"
+}
diff --git a/core/java/android/companion/DeviceId.java b/core/java/android/companion/DeviceId.java
index f66a1ae..d9514a0 100644
--- a/core/java/android/companion/DeviceId.java
+++ b/core/java/android/companion/DeviceId.java
@@ -154,6 +154,10 @@
 
     /**
      * A builder for {@link DeviceId}
+     *
+     * <p>Calling apps must provide at least one of the following to identify
+     * the device: a custom ID using {@link #setCustomId(String)}, or a MAC address using
+     * {@link #setMacAddress(MacAddress)}.</p>
      */
     public static final class Builder extends OneTimeUseBuilder<DeviceId> {
         private String mCustomId;
diff --git a/core/java/android/hardware/contexthub/HubEndpointSession.java b/core/java/android/hardware/contexthub/HubEndpointSession.java
index b8af398..77f937e 100644
--- a/core/java/android/hardware/contexthub/HubEndpointSession.java
+++ b/core/java/android/hardware/contexthub/HubEndpointSession.java
@@ -69,6 +69,8 @@
      * @return For messages that does not require a response, the transaction will immediately
      *     complete. For messages that requires a response, the transaction will complete after
      *     receiving the response for the message.
+     * @throws SecurityException if the application doesn't have the right permissions to send this
+     *     message.
      */
     @NonNull
     @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java
index 310e1a6..d9888ad 100644
--- a/core/java/android/hardware/location/ContextHubManager.java
+++ b/core/java/android/hardware/location/ContextHubManager.java
@@ -841,6 +841,7 @@
      * @param endpointId The identifier of the hub endpoint.
      * @param callback The callback to be invoked.
      * @param executor The executor to invoke the callback on.
+     * @throws UnsupportedOperationException If the operation is not supported.
      */
     @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @FlaggedApi(Flags.FLAG_OFFLOAD_API)
@@ -881,6 +882,7 @@
      * @param callback The callback to be invoked.
      * @param executor The executor to invoke the callback on.
      * @throws IllegalArgumentException if the serviceDescriptor is empty.
+     * @throws UnsupportedOperationException If the operation is not supported.
      */
     @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @FlaggedApi(Flags.FLAG_OFFLOAD_API)
@@ -911,6 +913,7 @@
      *
      * @param callback The callback previously registered.
      * @throws IllegalArgumentException If the callback was not previously registered.
+     * @throws UnsupportedOperationException If the operation is not supported.
      */
     @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @FlaggedApi(Flags.FLAG_OFFLOAD_API)
@@ -1311,6 +1314,8 @@
      *     endpoint discovery results (e.g. from {@link ContextHubManager#findEndpoints(long)}).
      * @param serviceDescriptor A string that describes the service associated with this session.
      *     The information will be sent to the destination as part of open request.
+     * @throws IllegalStateException if hubEndpoint was not successfully registered, or if there is
+     *     insufficient capacity for creating a session.
      */
     @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @FlaggedApi(Flags.FLAG_OFFLOAD_API)
diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
index 11b80ce..ce56a4f 100644
--- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java
+++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
@@ -1353,6 +1353,69 @@
         }
     }
 
+    /**
+     * @return true if we are blocked on a sync barrier
+     */
+    boolean isBlockedOnSyncBarrier() {
+        throwIfNotTest();
+        if (mUseConcurrent) {
+            Iterator<MessageNode> queueIter = mPriorityQueue.iterator();
+            MessageNode queueNode = iterateNext(queueIter);
+
+            if (queueNode.isBarrier()) {
+                long now = SystemClock.uptimeMillis();
+
+                /* Look for a deliverable async node. If one exists we are not blocked. */
+                Iterator<MessageNode> asyncQueueIter = mAsyncPriorityQueue.iterator();
+                MessageNode asyncNode = iterateNext(asyncQueueIter);
+                if (asyncNode != null && now >= asyncNode.getWhen()) {
+                    return false;
+                }
+                /*
+                 * Look for a deliverable sync node. In this case, if one exists we are blocked
+                 * since the barrier prevents delivery of the Message.
+                 */
+                while (queueNode.isBarrier()) {
+                    queueNode = iterateNext(queueIter);
+                }
+                if (queueNode != null && now >= queueNode.getWhen()) {
+                    return true;
+                }
+
+                return false;
+            }
+        } else {
+            Message msg = mMessages;
+            if (msg != null && msg.target == null) {
+                Message iter = msg;
+                /* Look for a deliverable async node */
+                do {
+                    iter = iter.next;
+                } while (iter != null && !iter.isAsynchronous());
+
+                long now = SystemClock.uptimeMillis();
+                if (iter != null && now >= iter.when) {
+                    return false;
+                }
+                /*
+                 * Look for a deliverable sync node. In this case, if one exists we are blocked
+                 * since the barrier prevents delivery of the Message.
+                 */
+                iter = msg;
+                do {
+                    iter = iter.next;
+                } while (iter != null && (iter.target == null || iter.isAsynchronous()));
+
+                if (iter != null && now >= iter.when) {
+                    return true;
+                }
+                return false;
+            }
+        }
+        /* No barrier was found. */
+        return false;
+    }
+
     private static final class MatchHandlerWhatAndObject extends MessageCompare {
         @Override
         public boolean compareMessage(MessageNode n, Handler h, int what, Object object, Runnable r,
diff --git a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
index 47778ed..576c4cc 100644
--- a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
@@ -1107,6 +1107,38 @@
         return nextMessage(false);
     }
 
+    /**
+     * @return true if we are blocked on a sync barrier
+     */
+    boolean isBlockedOnSyncBarrier() {
+        throwIfNotTest();
+        Iterator<MessageNode> queueIter = mPriorityQueue.iterator();
+        MessageNode queueNode = iterateNext(queueIter);
+
+        if (queueNode.isBarrier()) {
+            long now = SystemClock.uptimeMillis();
+
+            /* Look for a deliverable async node. If one exists we are not blocked. */
+            Iterator<MessageNode> asyncQueueIter = mAsyncPriorityQueue.iterator();
+            MessageNode asyncNode = iterateNext(asyncQueueIter);
+            if (asyncNode != null && now >= asyncNode.getWhen()) {
+                return false;
+            }
+            /*
+             * Look for a deliverable sync node. In this case, if one exists we are blocked
+             * since the barrier prevents delivery of the Message.
+             */
+            while (queueNode.isBarrier()) {
+                queueNode = iterateNext(queueIter);
+            }
+            if (queueNode != null && now >= queueNode.getWhen()) {
+                return true;
+            }
+
+            return false;
+        }
+    }
+
     private StateNode getStateNode(StackNode node) {
         if (node.isMessageNode()) {
             return ((MessageNode) node).mBottomOfStack;
diff --git a/core/java/android/os/LegacyMessageQueue/MessageQueue.java b/core/java/android/os/LegacyMessageQueue/MessageQueue.java
index f49acd1..10d0904 100644
--- a/core/java/android/os/LegacyMessageQueue/MessageQueue.java
+++ b/core/java/android/os/LegacyMessageQueue/MessageQueue.java
@@ -812,6 +812,40 @@
         return legacyPeekOrPop(false);
     }
 
+    /**
+     * @return true if we are blocked on a sync barrier
+     */
+    boolean isBlockedOnSyncBarrier() {
+        throwIfNotTest();
+        Message msg = mMessages;
+        if (msg != null && msg.target == null) {
+            Message iter = msg;
+            /* Look for a deliverable async node */
+            do {
+                iter = iter.next;
+            } while (iter != null && !iter.isAsynchronous());
+
+            long now = SystemClock.uptimeMillis();
+            if (iter != null && now >= iter.when) {
+                return false;
+            }
+            /*
+                * Look for a deliverable sync node. In this case, if one exists we are blocked
+                * since the barrier prevents delivery of the Message.
+                */
+            iter = msg;
+            do {
+                iter = iter.next;
+            } while (iter != null && (iter.target == null || iter.isAsynchronous()));
+
+            if (iter != null && now >= iter.when) {
+                return true;
+            }
+            return false;
+        }
+        return false;
+    }
+
     boolean hasMessages(Handler h, int what, Object object) {
         if (h == null) {
             return false;
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index d7e7ff2..cf473ec 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -50,7 +50,6 @@
 
 import dalvik.annotation.optimization.CriticalNative;
 import dalvik.annotation.optimization.FastNative;
-import dalvik.annotation.optimization.NeverInline;
 
 import libcore.util.SneakyThrow;
 
@@ -588,17 +587,6 @@
         return parcel;
     }
 
-    @NeverInline
-    private void errorUsedWhileRecycling() {
-        Log.wtf(TAG, "Parcel used while recycled. "
-                + Log.getStackTraceString(new Throwable())
-                + " Original recycle call (if DEBUG_RECYCLE): ", mStack);
-    }
-
-    private void assertNotRecycled() {
-        if (mRecycled) errorUsedWhileRecycling();
-    }
-
     /**
      * Put a Parcel object back into the pool.  You must not touch
      * the object after this call.
@@ -647,7 +635,6 @@
      * @hide
      */
     public void setReadWriteHelper(@Nullable ReadWriteHelper helper) {
-        assertNotRecycled();
         mReadWriteHelper = helper != null ? helper : ReadWriteHelper.DEFAULT;
     }
 
@@ -657,7 +644,6 @@
      * @hide
      */
     public boolean hasReadWriteHelper() {
-        assertNotRecycled();
         return (mReadWriteHelper != null) && (mReadWriteHelper != ReadWriteHelper.DEFAULT);
     }
 
@@ -684,7 +670,6 @@
      * @hide
      */
     public final void markSensitive() {
-        assertNotRecycled();
         nativeMarkSensitive(mNativePtr);
     }
 
@@ -701,7 +686,6 @@
      * @hide
      */
     public final boolean isForRpc() {
-        assertNotRecycled();
         return nativeIsForRpc(mNativePtr);
     }
 
@@ -709,25 +693,21 @@
     @ParcelFlags
     @TestApi
     public int getFlags() {
-        assertNotRecycled();
         return mFlags;
     }
 
     /** @hide */
     public void setFlags(@ParcelFlags int flags) {
-        assertNotRecycled();
         mFlags = flags;
     }
 
     /** @hide */
     public void addFlags(@ParcelFlags int flags) {
-        assertNotRecycled();
         mFlags |= flags;
     }
 
     /** @hide */
     private boolean hasFlags(@ParcelFlags int flags) {
-        assertNotRecycled();
         return (mFlags & flags) == flags;
     }
 
@@ -740,7 +720,6 @@
     // We don't really need to protect it; even if 3p / non-system apps, nothing would happen.
     // This would only work when used on a reply parcel by a binder object that's allowed-blocking.
     public void setPropagateAllowBlocking() {
-        assertNotRecycled();
         addFlags(FLAG_PROPAGATE_ALLOW_BLOCKING);
     }
 
@@ -748,7 +727,6 @@
      * Returns the total amount of data contained in the parcel.
      */
     public int dataSize() {
-        assertNotRecycled();
         return nativeDataSize(mNativePtr);
     }
 
@@ -757,7 +735,6 @@
      * parcel.  That is, {@link #dataSize}-{@link #dataPosition}.
      */
     public final int dataAvail() {
-        assertNotRecycled();
         return nativeDataAvail(mNativePtr);
     }
 
@@ -766,7 +743,6 @@
      * more than {@link #dataSize}.
      */
     public final int dataPosition() {
-        assertNotRecycled();
         return nativeDataPosition(mNativePtr);
     }
 
@@ -777,7 +753,6 @@
      * data buffer.
      */
     public final int dataCapacity() {
-        assertNotRecycled();
         return nativeDataCapacity(mNativePtr);
     }
 
@@ -789,7 +764,6 @@
      * @param size The new number of bytes in the Parcel.
      */
     public final void setDataSize(int size) {
-        assertNotRecycled();
         nativeSetDataSize(mNativePtr, size);
     }
 
@@ -799,7 +773,6 @@
      * {@link #dataSize}.
      */
     public final void setDataPosition(int pos) {
-        assertNotRecycled();
         nativeSetDataPosition(mNativePtr, pos);
     }
 
@@ -811,13 +784,11 @@
      * with this method.
      */
     public final void setDataCapacity(int size) {
-        assertNotRecycled();
         nativeSetDataCapacity(mNativePtr, size);
     }
 
     /** @hide */
     public final boolean pushAllowFds(boolean allowFds) {
-        assertNotRecycled();
         return nativePushAllowFds(mNativePtr, allowFds);
     }
 
@@ -838,7 +809,6 @@
      * in different versions of the platform.
      */
     public final byte[] marshall() {
-        assertNotRecycled();
         return nativeMarshall(mNativePtr);
     }
 
@@ -846,18 +816,15 @@
      * Fills the raw bytes of this Parcel with the supplied data.
      */
     public final void unmarshall(@NonNull byte[] data, int offset, int length) {
-        assertNotRecycled();
         nativeUnmarshall(mNativePtr, data, offset, length);
     }
 
     public final void appendFrom(Parcel parcel, int offset, int length) {
-        assertNotRecycled();
         nativeAppendFrom(mNativePtr, parcel.mNativePtr, offset, length);
     }
 
     /** @hide */
     public int compareData(Parcel other) {
-        assertNotRecycled();
         return nativeCompareData(mNativePtr, other.mNativePtr);
     }
 
@@ -868,7 +835,6 @@
 
     /** @hide */
     public final void setClassCookie(Class clz, Object cookie) {
-        assertNotRecycled();
         if (mClassCookies == null) {
             mClassCookies = new ArrayMap<>();
         }
@@ -878,13 +844,11 @@
     /** @hide */
     @Nullable
     public final Object getClassCookie(Class clz) {
-        assertNotRecycled();
         return mClassCookies != null ? mClassCookies.get(clz) : null;
     }
 
     /** @hide */
     public void removeClassCookie(Class clz, Object expectedCookie) {
-        assertNotRecycled();
         if (mClassCookies != null) {
             Object removedCookie = mClassCookies.remove(clz);
             if (removedCookie != expectedCookie) {
@@ -902,25 +866,21 @@
      * @hide
      */
     public boolean hasClassCookie(Class clz) {
-        assertNotRecycled();
         return mClassCookies != null && mClassCookies.containsKey(clz);
     }
 
     /** @hide */
     public final void adoptClassCookies(Parcel from) {
-        assertNotRecycled();
         mClassCookies = from.mClassCookies;
     }
 
     /** @hide */
     public Map<Class, Object> copyClassCookies() {
-        assertNotRecycled();
         return new ArrayMap<>(mClassCookies);
     }
 
     /** @hide */
     public void putClassCookies(Map<Class, Object> cookies) {
-        assertNotRecycled();
         if (cookies == null) {
             return;
         }
@@ -940,7 +900,6 @@
      * if the return value changes.
      */
     public boolean hasFileDescriptors() {
-        assertNotRecycled();
         return nativeHasFileDescriptors(mNativePtr);
     }
 
@@ -962,7 +921,6 @@
      * @throws IllegalArgumentException if the parameters are out of the permitted ranges.
      */
     public boolean hasFileDescriptors(int offset, int length) {
-        assertNotRecycled();
         return nativeHasFileDescriptorsInRange(mNativePtr, offset, length);
     }
 
@@ -1060,7 +1018,6 @@
      * @hide
      */
     public boolean hasBinders() {
-        assertNotRecycled();
         return nativeHasBinders(mNativePtr);
     }
 
@@ -1084,7 +1041,6 @@
      * @hide
      */
     public boolean hasBinders(int offset, int length) {
-        assertNotRecycled();
         return nativeHasBindersInRange(mNativePtr, offset, length);
     }
 
@@ -1095,7 +1051,6 @@
      * at the beginning of transactions as a header.
      */
     public final void writeInterfaceToken(@NonNull String interfaceName) {
-        assertNotRecycled();
         nativeWriteInterfaceToken(mNativePtr, interfaceName);
     }
 
@@ -1106,7 +1061,6 @@
      * should propagate to the caller.
      */
     public final void enforceInterface(@NonNull String interfaceName) {
-        assertNotRecycled();
         nativeEnforceInterface(mNativePtr, interfaceName);
     }
 
@@ -1117,7 +1071,6 @@
      * When used over binder, this exception should propagate to the caller.
      */
     public void enforceNoDataAvail() {
-        assertNotRecycled();
         final int n = dataAvail();
         if (n > 0) {
             throw new BadParcelableException("Parcel data not fully consumed, unread size: " + n);
@@ -1134,7 +1087,6 @@
      * @hide
      */
     public boolean replaceCallingWorkSourceUid(int workSourceUid) {
-        assertNotRecycled();
         return nativeReplaceCallingWorkSourceUid(mNativePtr, workSourceUid);
     }
 
@@ -1151,7 +1103,6 @@
      * @hide
      */
     public int readCallingWorkSourceUid() {
-        assertNotRecycled();
         return nativeReadCallingWorkSourceUid(mNativePtr);
     }
 
@@ -1161,7 +1112,6 @@
      * @param b Bytes to place into the parcel.
      */
     public final void writeByteArray(@Nullable byte[] b) {
-        assertNotRecycled();
         writeByteArray(b, 0, (b != null) ? b.length : 0);
     }
 
@@ -1173,7 +1123,6 @@
      * @param len Number of bytes to write.
      */
     public final void writeByteArray(@Nullable byte[] b, int offset, int len) {
-        assertNotRecycled();
         if (b == null) {
             writeInt(-1);
             return;
@@ -1195,7 +1144,6 @@
      * @see #readBlob()
      */
     public final void writeBlob(@Nullable byte[] b) {
-        assertNotRecycled();
         writeBlob(b, 0, (b != null) ? b.length : 0);
     }
 
@@ -1214,7 +1162,6 @@
      * @see #readBlob()
      */
     public final void writeBlob(@Nullable byte[] b, int offset, int len) {
-        assertNotRecycled();
         if (b == null) {
             writeInt(-1);
             return;
@@ -1233,7 +1180,6 @@
      * growing dataCapacity() if needed.
      */
     public final void writeInt(int val) {
-        assertNotRecycled();
         int err = nativeWriteInt(mNativePtr, val);
         if (err != OK) {
             nativeSignalExceptionForError(err);
@@ -1245,7 +1191,6 @@
      * growing dataCapacity() if needed.
      */
     public final void writeLong(long val) {
-        assertNotRecycled();
         int err = nativeWriteLong(mNativePtr, val);
         if (err != OK) {
             nativeSignalExceptionForError(err);
@@ -1257,7 +1202,6 @@
      * dataPosition(), growing dataCapacity() if needed.
      */
     public final void writeFloat(float val) {
-        assertNotRecycled();
         int err = nativeWriteFloat(mNativePtr, val);
         if (err != OK) {
             nativeSignalExceptionForError(err);
@@ -1269,7 +1213,6 @@
      * current dataPosition(), growing dataCapacity() if needed.
      */
     public final void writeDouble(double val) {
-        assertNotRecycled();
         int err = nativeWriteDouble(mNativePtr, val);
         if (err != OK) {
             nativeSignalExceptionForError(err);
@@ -1281,19 +1224,16 @@
      * growing dataCapacity() if needed.
      */
     public final void writeString(@Nullable String val) {
-        assertNotRecycled();
         writeString16(val);
     }
 
     /** {@hide} */
     public final void writeString8(@Nullable String val) {
-        assertNotRecycled();
         mReadWriteHelper.writeString8(this, val);
     }
 
     /** {@hide} */
     public final void writeString16(@Nullable String val) {
-        assertNotRecycled();
         mReadWriteHelper.writeString16(this, val);
     }
 
@@ -1305,19 +1245,16 @@
      * @hide
      */
     public void writeStringNoHelper(@Nullable String val) {
-        assertNotRecycled();
         writeString16NoHelper(val);
     }
 
     /** {@hide} */
     public void writeString8NoHelper(@Nullable String val) {
-        assertNotRecycled();
         nativeWriteString8(mNativePtr, val);
     }
 
     /** {@hide} */
     public void writeString16NoHelper(@Nullable String val) {
-        assertNotRecycled();
         nativeWriteString16(mNativePtr, val);
     }
 
@@ -1329,7 +1266,6 @@
      * for true or false, respectively, but may change in the future.
      */
     public final void writeBoolean(boolean val) {
-        assertNotRecycled();
         writeInt(val ? 1 : 0);
     }
 
@@ -1341,7 +1277,6 @@
     @UnsupportedAppUsage
     @RavenwoodThrow(blockedBy = android.text.Spanned.class)
     public final void writeCharSequence(@Nullable CharSequence val) {
-        assertNotRecycled();
         TextUtils.writeToParcel(val, this, 0);
     }
 
@@ -1350,7 +1285,6 @@
      * growing dataCapacity() if needed.
      */
     public final void writeStrongBinder(IBinder val) {
-        assertNotRecycled();
         nativeWriteStrongBinder(mNativePtr, val);
     }
 
@@ -1359,7 +1293,6 @@
      * growing dataCapacity() if needed.
      */
     public final void writeStrongInterface(IInterface val) {
-        assertNotRecycled();
         writeStrongBinder(val == null ? null : val.asBinder());
     }
 
@@ -1374,7 +1307,6 @@
      * if {@link Parcelable#PARCELABLE_WRITE_RETURN_VALUE} is set.</p>
      */
     public final void writeFileDescriptor(@NonNull FileDescriptor val) {
-        assertNotRecycled();
         nativeWriteFileDescriptor(mNativePtr, val);
     }
 
@@ -1383,7 +1315,6 @@
      * This will be the new name for writeFileDescriptor, for consistency.
      **/
     public final void writeRawFileDescriptor(@NonNull FileDescriptor val) {
-        assertNotRecycled();
         nativeWriteFileDescriptor(mNativePtr, val);
     }
 
@@ -1394,7 +1325,6 @@
      * @param value The array of objects to be written.
      */
     public final void writeRawFileDescriptorArray(@Nullable FileDescriptor[] value) {
-        assertNotRecycled();
         if (value != null) {
             int N = value.length;
             writeInt(N);
@@ -1414,7 +1344,6 @@
      * the future.
      */
     public final void writeByte(byte val) {
-        assertNotRecycled();
         writeInt(val);
     }
 
@@ -1430,7 +1359,6 @@
      * allows you to avoid mysterious type errors at the point of marshalling.
      */
     public final void writeMap(@Nullable Map val) {
-        assertNotRecycled();
         writeMapInternal((Map<String, Object>) val);
     }
 
@@ -1439,7 +1367,6 @@
      * growing dataCapacity() if needed.  The Map keys must be String objects.
      */
     /* package */ void writeMapInternal(@Nullable Map<String,Object> val) {
-        assertNotRecycled();
         if (val == null) {
             writeInt(-1);
             return;
@@ -1465,7 +1392,6 @@
      * growing dataCapacity() if needed.  The Map keys must be String objects.
      */
     /* package */ void writeArrayMapInternal(@Nullable ArrayMap<String, Object> val) {
-        assertNotRecycled();
         if (val == null) {
             writeInt(-1);
             return;
@@ -1495,7 +1421,6 @@
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public void writeArrayMap(@Nullable ArrayMap<String, Object> val) {
-        assertNotRecycled();
         writeArrayMapInternal(val);
     }
 
@@ -1514,7 +1439,6 @@
      */
     public <T extends Parcelable> void writeTypedArrayMap(@Nullable ArrayMap<String, T> val,
             int parcelableFlags) {
-        assertNotRecycled();
         if (val == null) {
             writeInt(-1);
             return;
@@ -1536,7 +1460,6 @@
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public void writeArraySet(@Nullable ArraySet<? extends Object> val) {
-        assertNotRecycled();
         final int size = (val != null) ? val.size() : -1;
         writeInt(size);
         for (int i = 0; i < size; i++) {
@@ -1549,7 +1472,6 @@
      * growing dataCapacity() if needed.
      */
     public final void writeBundle(@Nullable Bundle val) {
-        assertNotRecycled();
         if (val == null) {
             writeInt(-1);
             return;
@@ -1563,7 +1485,6 @@
      * growing dataCapacity() if needed.
      */
     public final void writePersistableBundle(@Nullable PersistableBundle val) {
-        assertNotRecycled();
         if (val == null) {
             writeInt(-1);
             return;
@@ -1577,7 +1498,6 @@
      * growing dataCapacity() if needed.
      */
     public final void writeSize(@NonNull Size val) {
-        assertNotRecycled();
         writeInt(val.getWidth());
         writeInt(val.getHeight());
     }
@@ -1587,7 +1507,6 @@
      * growing dataCapacity() if needed.
      */
     public final void writeSizeF(@NonNull SizeF val) {
-        assertNotRecycled();
         writeFloat(val.getWidth());
         writeFloat(val.getHeight());
     }
@@ -1598,7 +1517,6 @@
      * {@link #writeValue} and must follow the specification there.
      */
     public final void writeList(@Nullable List val) {
-        assertNotRecycled();
         if (val == null) {
             writeInt(-1);
             return;
@@ -1618,7 +1536,6 @@
      * {@link #writeValue} and must follow the specification there.
      */
     public final void writeArray(@Nullable Object[] val) {
-        assertNotRecycled();
         if (val == null) {
             writeInt(-1);
             return;
@@ -1639,7 +1556,6 @@
      * specification there.
      */
     public final <T> void writeSparseArray(@Nullable SparseArray<T> val) {
-        assertNotRecycled();
         if (val == null) {
             writeInt(-1);
             return;
@@ -1655,7 +1571,6 @@
     }
 
     public final void writeSparseBooleanArray(@Nullable SparseBooleanArray val) {
-        assertNotRecycled();
         if (val == null) {
             writeInt(-1);
             return;
@@ -1674,7 +1589,6 @@
      * @hide
      */
     public final void writeSparseIntArray(@Nullable SparseIntArray val) {
-        assertNotRecycled();
         if (val == null) {
             writeInt(-1);
             return;
@@ -1690,7 +1604,6 @@
     }
 
     public final void writeBooleanArray(@Nullable boolean[] val) {
-        assertNotRecycled();
         if (val != null) {
             int N = val.length;
             writeInt(N);
@@ -1725,7 +1638,6 @@
     }
 
     private void ensureWithinMemoryLimit(int typeSize, @NonNull int... dimensions) {
-        assertNotRecycled();
         // For Multidimensional arrays, Calculate total object
         // which will be allocated.
         int totalObjects = 1;
@@ -1743,7 +1655,6 @@
     }
 
     private void ensureWithinMemoryLimit(int typeSize, int length) {
-        assertNotRecycled();
         int estimatedAllocationSize = 0;
         try {
             estimatedAllocationSize = Math.multiplyExact(typeSize, length);
@@ -1767,7 +1678,6 @@
 
     @Nullable
     public final boolean[] createBooleanArray() {
-        assertNotRecycled();
         int N = readInt();
         ensureWithinMemoryLimit(SIZE_BOOLEAN, N);
         // >>2 as a fast divide-by-4 works in the create*Array() functions
@@ -1785,7 +1695,6 @@
     }
 
     public final void readBooleanArray(@NonNull boolean[] val) {
-        assertNotRecycled();
         int N = readInt();
         if (N == val.length) {
             for (int i=0; i<N; i++) {
@@ -1798,7 +1707,6 @@
 
     /** @hide */
     public void writeShortArray(@Nullable short[] val) {
-        assertNotRecycled();
         if (val != null) {
             int n = val.length;
             writeInt(n);
@@ -1813,7 +1721,6 @@
     /** @hide */
     @Nullable
     public short[] createShortArray() {
-        assertNotRecycled();
         int n = readInt();
         ensureWithinMemoryLimit(SIZE_SHORT, n);
         if (n >= 0 && n <= (dataAvail() >> 2)) {
@@ -1829,7 +1736,6 @@
 
     /** @hide */
     public void readShortArray(@NonNull short[] val) {
-        assertNotRecycled();
         int n = readInt();
         if (n == val.length) {
             for (int i = 0; i < n; i++) {
@@ -1841,7 +1747,6 @@
     }
 
     public final void writeCharArray(@Nullable char[] val) {
-        assertNotRecycled();
         if (val != null) {
             int N = val.length;
             writeInt(N);
@@ -1855,7 +1760,6 @@
 
     @Nullable
     public final char[] createCharArray() {
-        assertNotRecycled();
         int N = readInt();
         ensureWithinMemoryLimit(SIZE_CHAR, N);
         if (N >= 0 && N <= (dataAvail() >> 2)) {
@@ -1870,7 +1774,6 @@
     }
 
     public final void readCharArray(@NonNull char[] val) {
-        assertNotRecycled();
         int N = readInt();
         if (N == val.length) {
             for (int i=0; i<N; i++) {
@@ -1882,7 +1785,6 @@
     }
 
     public final void writeIntArray(@Nullable int[] val) {
-        assertNotRecycled();
         if (val != null) {
             int N = val.length;
             writeInt(N);
@@ -1896,7 +1798,6 @@
 
     @Nullable
     public final int[] createIntArray() {
-        assertNotRecycled();
         int N = readInt();
         ensureWithinMemoryLimit(SIZE_INT, N);
         if (N >= 0 && N <= (dataAvail() >> 2)) {
@@ -1911,7 +1812,6 @@
     }
 
     public final void readIntArray(@NonNull int[] val) {
-        assertNotRecycled();
         int N = readInt();
         if (N == val.length) {
             for (int i=0; i<N; i++) {
@@ -1923,7 +1823,6 @@
     }
 
     public final void writeLongArray(@Nullable long[] val) {
-        assertNotRecycled();
         if (val != null) {
             int N = val.length;
             writeInt(N);
@@ -1937,7 +1836,6 @@
 
     @Nullable
     public final long[] createLongArray() {
-        assertNotRecycled();
         int N = readInt();
         ensureWithinMemoryLimit(SIZE_LONG, N);
         // >>3 because stored longs are 64 bits
@@ -1953,7 +1851,6 @@
     }
 
     public final void readLongArray(@NonNull long[] val) {
-        assertNotRecycled();
         int N = readInt();
         if (N == val.length) {
             for (int i=0; i<N; i++) {
@@ -1965,7 +1862,6 @@
     }
 
     public final void writeFloatArray(@Nullable float[] val) {
-        assertNotRecycled();
         if (val != null) {
             int N = val.length;
             writeInt(N);
@@ -1979,7 +1875,6 @@
 
     @Nullable
     public final float[] createFloatArray() {
-        assertNotRecycled();
         int N = readInt();
         ensureWithinMemoryLimit(SIZE_FLOAT, N);
         // >>2 because stored floats are 4 bytes
@@ -1995,7 +1890,6 @@
     }
 
     public final void readFloatArray(@NonNull float[] val) {
-        assertNotRecycled();
         int N = readInt();
         if (N == val.length) {
             for (int i=0; i<N; i++) {
@@ -2007,7 +1901,6 @@
     }
 
     public final void writeDoubleArray(@Nullable double[] val) {
-        assertNotRecycled();
         if (val != null) {
             int N = val.length;
             writeInt(N);
@@ -2021,7 +1914,6 @@
 
     @Nullable
     public final double[] createDoubleArray() {
-        assertNotRecycled();
         int N = readInt();
         ensureWithinMemoryLimit(SIZE_DOUBLE, N);
         // >>3 because stored doubles are 8 bytes
@@ -2037,7 +1929,6 @@
     }
 
     public final void readDoubleArray(@NonNull double[] val) {
-        assertNotRecycled();
         int N = readInt();
         if (N == val.length) {
             for (int i=0; i<N; i++) {
@@ -2049,24 +1940,20 @@
     }
 
     public final void writeStringArray(@Nullable String[] val) {
-        assertNotRecycled();
         writeString16Array(val);
     }
 
     @Nullable
     public final String[] createStringArray() {
-        assertNotRecycled();
         return createString16Array();
     }
 
     public final void readStringArray(@NonNull String[] val) {
-        assertNotRecycled();
         readString16Array(val);
     }
 
     /** {@hide} */
     public final void writeString8Array(@Nullable String[] val) {
-        assertNotRecycled();
         if (val != null) {
             int N = val.length;
             writeInt(N);
@@ -2081,7 +1968,6 @@
     /** {@hide} */
     @Nullable
     public final String[] createString8Array() {
-        assertNotRecycled();
         int N = readInt();
         ensureWithinMemoryLimit(SIZE_COMPLEX_TYPE, N);
         if (N >= 0) {
@@ -2097,7 +1983,6 @@
 
     /** {@hide} */
     public final void readString8Array(@NonNull String[] val) {
-        assertNotRecycled();
         int N = readInt();
         if (N == val.length) {
             for (int i=0; i<N; i++) {
@@ -2110,7 +1995,6 @@
 
     /** {@hide} */
     public final void writeString16Array(@Nullable String[] val) {
-        assertNotRecycled();
         if (val != null) {
             int N = val.length;
             writeInt(N);
@@ -2125,7 +2009,6 @@
     /** {@hide} */
     @Nullable
     public final String[] createString16Array() {
-        assertNotRecycled();
         int N = readInt();
         ensureWithinMemoryLimit(SIZE_COMPLEX_TYPE, N);
         if (N >= 0) {
@@ -2141,7 +2024,6 @@
 
     /** {@hide} */
     public final void readString16Array(@NonNull String[] val) {
-        assertNotRecycled();
         int N = readInt();
         if (N == val.length) {
             for (int i=0; i<N; i++) {
@@ -2153,7 +2035,6 @@
     }
 
     public final void writeBinderArray(@Nullable IBinder[] val) {
-        assertNotRecycled();
         if (val != null) {
             int N = val.length;
             writeInt(N);
@@ -2178,7 +2059,6 @@
      */
     public final <T extends IInterface> void writeInterfaceArray(
             @SuppressLint("ArrayReturn") @Nullable T[] val) {
-        assertNotRecycled();
         if (val != null) {
             int N = val.length;
             writeInt(N);
@@ -2194,7 +2074,6 @@
      * @hide
      */
     public final void writeCharSequenceArray(@Nullable CharSequence[] val) {
-        assertNotRecycled();
         if (val != null) {
             int N = val.length;
             writeInt(N);
@@ -2210,7 +2089,6 @@
      * @hide
      */
     public final void writeCharSequenceList(@Nullable ArrayList<CharSequence> val) {
-        assertNotRecycled();
         if (val != null) {
             int N = val.size();
             writeInt(N);
@@ -2224,7 +2102,6 @@
 
     @Nullable
     public final IBinder[] createBinderArray() {
-        assertNotRecycled();
         int N = readInt();
         ensureWithinMemoryLimit(SIZE_COMPLEX_TYPE, N);
         if (N >= 0) {
@@ -2239,7 +2116,6 @@
     }
 
     public final void readBinderArray(@NonNull IBinder[] val) {
-        assertNotRecycled();
         int N = readInt();
         if (N == val.length) {
             for (int i=0; i<N; i++) {
@@ -2261,7 +2137,6 @@
     @Nullable
     public final <T extends IInterface> T[] createInterfaceArray(
             @NonNull IntFunction<T[]> newArray, @NonNull Function<IBinder, T> asInterface) {
-        assertNotRecycled();
         int N = readInt();
         ensureWithinMemoryLimit(SIZE_COMPLEX_TYPE, N);
         if (N >= 0) {
@@ -2286,7 +2161,6 @@
     public final <T extends IInterface> void readInterfaceArray(
             @SuppressLint("ArrayReturn") @NonNull T[] val,
             @NonNull Function<IBinder, T> asInterface) {
-        assertNotRecycled();
         int N = readInt();
         if (N == val.length) {
             for (int i=0; i<N; i++) {
@@ -2312,7 +2186,6 @@
      * @see Parcelable
      */
     public final <T extends Parcelable> void writeTypedList(@Nullable List<T> val) {
-        assertNotRecycled();
         writeTypedList(val, 0);
     }
 
@@ -2332,7 +2205,6 @@
      */
     public final <T extends Parcelable> void writeTypedSparseArray(@Nullable SparseArray<T> val,
             int parcelableFlags) {
-        assertNotRecycled();
         if (val == null) {
             writeInt(-1);
             return;
@@ -2362,7 +2234,6 @@
      * @see Parcelable
      */
     public <T extends Parcelable> void writeTypedList(@Nullable List<T> val, int parcelableFlags) {
-        assertNotRecycled();
         if (val == null) {
             writeInt(-1);
             return;
@@ -2388,7 +2259,6 @@
      * @see #readStringList
      */
     public final void writeStringList(@Nullable List<String> val) {
-        assertNotRecycled();
         if (val == null) {
             writeInt(-1);
             return;
@@ -2414,7 +2284,6 @@
      * @see #readBinderList
      */
     public final void writeBinderList(@Nullable List<IBinder> val) {
-        assertNotRecycled();
         if (val == null) {
             writeInt(-1);
             return;
@@ -2437,7 +2306,6 @@
      * @see #readInterfaceList
      */
     public final <T extends IInterface> void writeInterfaceList(@Nullable List<T> val) {
-        assertNotRecycled();
         if (val == null) {
             writeInt(-1);
             return;
@@ -2459,7 +2327,6 @@
      * @see #readParcelableList(List, ClassLoader)
      */
     public final <T extends Parcelable> void writeParcelableList(@Nullable List<T> val, int flags) {
-        assertNotRecycled();
         if (val == null) {
             writeInt(-1);
             return;
@@ -2494,7 +2361,6 @@
      */
     public final <T extends Parcelable> void writeTypedArray(@Nullable T[] val,
             int parcelableFlags) {
-        assertNotRecycled();
         if (val != null) {
             int N = val.length;
             writeInt(N);
@@ -2517,7 +2383,6 @@
      */
     public final <T extends Parcelable> void writeTypedObject(@Nullable T val,
             int parcelableFlags) {
-        assertNotRecycled();
         if (val != null) {
             writeInt(1);
             val.writeToParcel(this, parcelableFlags);
@@ -2555,7 +2420,6 @@
      */
     public <T> void writeFixedArray(@Nullable T val, int parcelableFlags,
             @NonNull int... dimensions) {
-        assertNotRecycled();
         if (val == null) {
             writeInt(-1);
             return;
@@ -2667,7 +2531,6 @@
      * should be used).</p>
      */
     public final void writeValue(@Nullable Object v) {
-        assertNotRecycled();
         if (v instanceof LazyValue) {
             LazyValue value = (LazyValue) v;
             value.writeToParcel(this);
@@ -2785,7 +2648,6 @@
      * @hide
      */
     public void writeValue(int type, @Nullable Object v) {
-        assertNotRecycled();
         switch (type) {
             case VAL_NULL:
                 break;
@@ -2899,7 +2761,6 @@
      * {@link Parcelable#writeToParcel(Parcel, int) Parcelable.writeToParcel()}.
      */
     public final void writeParcelable(@Nullable Parcelable p, int parcelableFlags) {
-        assertNotRecycled();
         if (p == null) {
             writeString(null);
             return;
@@ -2915,7 +2776,6 @@
      * @see #readParcelableCreator
      */
     public final void writeParcelableCreator(@NonNull Parcelable p) {
-        assertNotRecycled();
         String name = p.getClass().getName();
         writeString(name);
     }
@@ -2954,7 +2814,6 @@
      */
     @TestApi
     public boolean allowSquashing() {
-        assertNotRecycled();
         boolean previous = mAllowSquashing;
         mAllowSquashing = true;
         return previous;
@@ -2966,7 +2825,6 @@
      */
     @TestApi
     public void restoreAllowSquashing(boolean previous) {
-        assertNotRecycled();
         mAllowSquashing = previous;
         if (!mAllowSquashing) {
             mWrittenSquashableParcelables = null;
@@ -3023,7 +2881,6 @@
      * @hide
      */
     public boolean maybeWriteSquashed(@NonNull Parcelable p) {
-        assertNotRecycled();
         if (!mAllowSquashing) {
             // Don't squash, and don't put it in the map either.
             writeInt(0);
@@ -3074,7 +2931,6 @@
     @SuppressWarnings("unchecked")
     @Nullable
     public <T extends Parcelable> T readSquashed(SquashReadHelper<T> reader) {
-        assertNotRecycled();
         final int offset = readInt();
         final int pos = dataPosition();
 
@@ -3108,7 +2964,6 @@
      * using the other approaches to writing data in to a Parcel.
      */
     public final void writeSerializable(@Nullable Serializable s) {
-        assertNotRecycled();
         if (s == null) {
             writeString(null);
             return;
@@ -3161,7 +3016,6 @@
      */
     @RavenwoodReplace(blockedBy = AppOpsManager.class)
     public final void writeException(@NonNull Exception e) {
-        assertNotRecycled();
         AppOpsManager.prefixParcelWithAppOpsIfNeeded(this);
 
         int code = getExceptionCode(e);
@@ -3242,7 +3096,6 @@
 
     /** @hide */
     public void writeStackTrace(@NonNull Throwable e) {
-        assertNotRecycled();
         final int sizePosition = dataPosition();
         writeInt(0); // Header size will be filled in later
         StackTraceElement[] stackTrace = e.getStackTrace();
@@ -3268,7 +3121,6 @@
      */
     @RavenwoodReplace(blockedBy = AppOpsManager.class)
     public final void writeNoException() {
-        assertNotRecycled();
         AppOpsManager.prefixParcelWithAppOpsIfNeeded(this);
 
         // Despite the name of this function ("write no exception"),
@@ -3312,7 +3164,6 @@
      * @see #writeNoException
      */
     public final void readException() {
-        assertNotRecycled();
         int code = readExceptionCode();
         if (code != 0) {
             String msg = readString();
@@ -3336,7 +3187,6 @@
     @UnsupportedAppUsage
     @TestApi
     public final int readExceptionCode() {
-        assertNotRecycled();
         int code = readInt();
         if (code == EX_HAS_NOTED_APPOPS_REPLY_HEADER) {
             AppOpsManager.readAndLogNotedAppops(this);
@@ -3370,7 +3220,6 @@
      * @param msg The exception message.
      */
     public final void readException(int code, String msg) {
-        assertNotRecycled();
         String remoteStackTrace = null;
         final int remoteStackPayloadSize = readInt();
         if (remoteStackPayloadSize > 0) {
@@ -3401,7 +3250,6 @@
 
     /** @hide */
     public Exception createExceptionOrNull(int code, String msg) {
-        assertNotRecycled();
         switch (code) {
             case EX_PARCELABLE:
                 if (readInt() > 0) {
@@ -3434,7 +3282,6 @@
      * Read an integer value from the parcel at the current dataPosition().
      */
     public final int readInt() {
-        assertNotRecycled();
         return nativeReadInt(mNativePtr);
     }
 
@@ -3442,7 +3289,6 @@
      * Read a long integer value from the parcel at the current dataPosition().
      */
     public final long readLong() {
-        assertNotRecycled();
         return nativeReadLong(mNativePtr);
     }
 
@@ -3451,7 +3297,6 @@
      * dataPosition().
      */
     public final float readFloat() {
-        assertNotRecycled();
         return nativeReadFloat(mNativePtr);
     }
 
@@ -3460,7 +3305,6 @@
      * current dataPosition().
      */
     public final double readDouble() {
-        assertNotRecycled();
         return nativeReadDouble(mNativePtr);
     }
 
@@ -3469,19 +3313,16 @@
      */
     @Nullable
     public final String readString() {
-        assertNotRecycled();
         return readString16();
     }
 
     /** {@hide} */
     public final @Nullable String readString8() {
-        assertNotRecycled();
         return mReadWriteHelper.readString8(this);
     }
 
     /** {@hide} */
     public final @Nullable String readString16() {
-        assertNotRecycled();
         return mReadWriteHelper.readString16(this);
     }
 
@@ -3493,19 +3334,16 @@
      * @hide
      */
     public @Nullable String readStringNoHelper() {
-        assertNotRecycled();
         return readString16NoHelper();
     }
 
     /** {@hide} */
     public @Nullable String readString8NoHelper() {
-        assertNotRecycled();
         return nativeReadString8(mNativePtr);
     }
 
     /** {@hide} */
     public @Nullable String readString16NoHelper() {
-        assertNotRecycled();
         return nativeReadString16(mNativePtr);
     }
 
@@ -3513,7 +3351,6 @@
      * Read a boolean value from the parcel at the current dataPosition().
      */
     public final boolean readBoolean() {
-        assertNotRecycled();
         return readInt() != 0;
     }
 
@@ -3524,7 +3361,6 @@
     @UnsupportedAppUsage
     @Nullable
     public final CharSequence readCharSequence() {
-        assertNotRecycled();
         return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(this);
     }
 
@@ -3532,7 +3368,6 @@
      * Read an object from the parcel at the current dataPosition().
      */
     public final IBinder readStrongBinder() {
-        assertNotRecycled();
         final IBinder result = nativeReadStrongBinder(mNativePtr);
 
         // If it's a reply from a method with @PropagateAllowBlocking, then inherit allow-blocking
@@ -3548,7 +3383,6 @@
      * Read a FileDescriptor from the parcel at the current dataPosition().
      */
     public final ParcelFileDescriptor readFileDescriptor() {
-        assertNotRecycled();
         FileDescriptor fd = nativeReadFileDescriptor(mNativePtr);
         return fd != null ? new ParcelFileDescriptor(fd) : null;
     }
@@ -3556,7 +3390,6 @@
     /** {@hide} */
     @UnsupportedAppUsage
     public final FileDescriptor readRawFileDescriptor() {
-        assertNotRecycled();
         return nativeReadFileDescriptor(mNativePtr);
     }
 
@@ -3567,7 +3400,6 @@
      **/
     @Nullable
     public final FileDescriptor[] createRawFileDescriptorArray() {
-        assertNotRecycled();
         int N = readInt();
         if (N < 0) {
             return null;
@@ -3587,7 +3419,6 @@
      * @return the FileDescriptor array, or null if the array is null.
      **/
     public final void readRawFileDescriptorArray(FileDescriptor[] val) {
-        assertNotRecycled();
         int N = readInt();
         if (N == val.length) {
             for (int i=0; i<N; i++) {
@@ -3602,7 +3433,6 @@
      * Read a byte value from the parcel at the current dataPosition().
      */
     public final byte readByte() {
-        assertNotRecycled();
         return (byte)(readInt() & 0xff);
     }
 
@@ -3617,7 +3447,6 @@
      */
     @Deprecated
     public final void readMap(@NonNull Map outVal, @Nullable ClassLoader loader) {
-        assertNotRecycled();
         readMapInternal(outVal, loader, /* clazzKey */ null, /* clazzValue */ null);
     }
 
@@ -3631,7 +3460,6 @@
     public <K, V> void readMap(@NonNull Map<? super K, ? super V> outVal,
             @Nullable ClassLoader loader, @NonNull Class<K> clazzKey,
             @NonNull Class<V> clazzValue) {
-        assertNotRecycled();
         Objects.requireNonNull(clazzKey);
         Objects.requireNonNull(clazzValue);
         readMapInternal(outVal, loader, clazzKey, clazzValue);
@@ -3650,7 +3478,6 @@
      */
     @Deprecated
     public final void readList(@NonNull List outVal, @Nullable ClassLoader loader) {
-        assertNotRecycled();
         int N = readInt();
         readListInternal(outVal, N, loader, /* clazz */ null);
     }
@@ -3672,7 +3499,6 @@
      */
     public <T> void readList(@NonNull List<? super T> outVal,
             @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
-        assertNotRecycled();
         Objects.requireNonNull(clazz);
         int n = readInt();
         readListInternal(outVal, n, loader, clazz);
@@ -3692,7 +3518,6 @@
     @Deprecated
     @Nullable
     public HashMap readHashMap(@Nullable ClassLoader loader) {
-        assertNotRecycled();
         return readHashMapInternal(loader, /* clazzKey */ null, /* clazzValue */ null);
     }
 
@@ -3707,7 +3532,6 @@
     @Nullable
     public <K, V> HashMap<K, V> readHashMap(@Nullable ClassLoader loader,
             @NonNull Class<? extends K> clazzKey, @NonNull Class<? extends V> clazzValue) {
-        assertNotRecycled();
         Objects.requireNonNull(clazzKey);
         Objects.requireNonNull(clazzValue);
         return readHashMapInternal(loader, clazzKey, clazzValue);
@@ -3720,7 +3544,6 @@
      */
     @Nullable
     public final Bundle readBundle() {
-        assertNotRecycled();
         return readBundle(null);
     }
 
@@ -3732,7 +3555,6 @@
      */
     @Nullable
     public final Bundle readBundle(@Nullable ClassLoader loader) {
-        assertNotRecycled();
         int length = readInt();
         if (length < 0) {
             if (Bundle.DEBUG) Log.d(TAG, "null bundle: length=" + length);
@@ -3753,7 +3575,6 @@
      */
     @Nullable
     public final PersistableBundle readPersistableBundle() {
-        assertNotRecycled();
         return readPersistableBundle(null);
     }
 
@@ -3765,7 +3586,6 @@
      */
     @Nullable
     public final PersistableBundle readPersistableBundle(@Nullable ClassLoader loader) {
-        assertNotRecycled();
         int length = readInt();
         if (length < 0) {
             if (Bundle.DEBUG) Log.d(TAG, "null bundle: length=" + length);
@@ -3784,7 +3604,6 @@
      */
     @NonNull
     public final Size readSize() {
-        assertNotRecycled();
         final int width = readInt();
         final int height = readInt();
         return new Size(width, height);
@@ -3795,7 +3614,6 @@
      */
     @NonNull
     public final SizeF readSizeF() {
-        assertNotRecycled();
         final float width = readFloat();
         final float height = readFloat();
         return new SizeF(width, height);
@@ -3806,7 +3624,6 @@
      */
     @Nullable
     public final byte[] createByteArray() {
-        assertNotRecycled();
         return nativeCreateByteArray(mNativePtr);
     }
 
@@ -3815,7 +3632,6 @@
      * given byte array.
      */
     public final void readByteArray(@NonNull byte[] val) {
-        assertNotRecycled();
         boolean valid = nativeReadByteArray(mNativePtr, val, (val != null) ? val.length : 0);
         if (!valid) {
             throw new RuntimeException("bad array lengths");
@@ -3828,7 +3644,6 @@
      */
     @Nullable
     public final byte[] readBlob() {
-        assertNotRecycled();
         return nativeReadBlob(mNativePtr);
     }
 
@@ -3839,7 +3654,6 @@
     @UnsupportedAppUsage
     @Nullable
     public final String[] readStringArray() {
-        assertNotRecycled();
         return createString16Array();
     }
 
@@ -3849,7 +3663,6 @@
      */
     @Nullable
     public final CharSequence[] readCharSequenceArray() {
-        assertNotRecycled();
         CharSequence[] array = null;
 
         int length = readInt();
@@ -3872,7 +3685,6 @@
      */
     @Nullable
     public final ArrayList<CharSequence> readCharSequenceList() {
-        assertNotRecycled();
         ArrayList<CharSequence> array = null;
 
         int length = readInt();
@@ -3902,7 +3714,6 @@
     @Deprecated
     @Nullable
     public ArrayList readArrayList(@Nullable ClassLoader loader) {
-        assertNotRecycled();
         return readArrayListInternal(loader, /* clazz */ null);
     }
 
@@ -3925,7 +3736,6 @@
     @Nullable
     public <T> ArrayList<T> readArrayList(@Nullable ClassLoader loader,
             @NonNull Class<? extends T> clazz) {
-        assertNotRecycled();
         Objects.requireNonNull(clazz);
         return readArrayListInternal(loader, clazz);
     }
@@ -3945,7 +3755,6 @@
     @Deprecated
     @Nullable
     public Object[] readArray(@Nullable ClassLoader loader) {
-        assertNotRecycled();
         return readArrayInternal(loader, /* clazz */ null);
     }
 
@@ -3967,7 +3776,6 @@
     @SuppressLint({"ArrayReturn", "NullableCollection"})
     @Nullable
     public <T> T[] readArray(@Nullable ClassLoader loader, @NonNull Class<T> clazz) {
-        assertNotRecycled();
         Objects.requireNonNull(clazz);
         return readArrayInternal(loader, clazz);
     }
@@ -3987,7 +3795,6 @@
     @Deprecated
     @Nullable
     public <T> SparseArray<T> readSparseArray(@Nullable ClassLoader loader) {
-        assertNotRecycled();
         return readSparseArrayInternal(loader, /* clazz */ null);
     }
 
@@ -4009,7 +3816,6 @@
     @Nullable
     public <T> SparseArray<T> readSparseArray(@Nullable ClassLoader loader,
             @NonNull Class<? extends T> clazz) {
-        assertNotRecycled();
         Objects.requireNonNull(clazz);
         return readSparseArrayInternal(loader, clazz);
     }
@@ -4021,7 +3827,6 @@
      */
     @Nullable
     public final SparseBooleanArray readSparseBooleanArray() {
-        assertNotRecycled();
         int N = readInt();
         if (N < 0) {
             return null;
@@ -4038,7 +3843,6 @@
      */
     @Nullable
     public final SparseIntArray readSparseIntArray() {
-        assertNotRecycled();
         int N = readInt();
         if (N < 0) {
             return null;
@@ -4063,7 +3867,6 @@
      */
     @Nullable
     public final <T> ArrayList<T> createTypedArrayList(@NonNull Parcelable.Creator<T> c) {
-        assertNotRecycled();
         int N = readInt();
         if (N < 0) {
             return null;
@@ -4087,7 +3890,6 @@
      * @see #writeTypedList
      */
     public final <T> void readTypedList(@NonNull List<T> list, @NonNull Parcelable.Creator<T> c) {
-        assertNotRecycled();
         int M = list.size();
         int N = readInt();
         int i = 0;
@@ -4117,7 +3919,6 @@
      */
     public final @Nullable <T extends Parcelable> SparseArray<T> createTypedSparseArray(
             @NonNull Parcelable.Creator<T> creator) {
-        assertNotRecycled();
         final int count = readInt();
         if (count < 0) {
             return null;
@@ -4147,7 +3948,6 @@
      */
     public final @Nullable <T extends Parcelable> ArrayMap<String, T> createTypedArrayMap(
             @NonNull Parcelable.Creator<T> creator) {
-        assertNotRecycled();
         final int count = readInt();
         if (count < 0) {
             return null;
@@ -4175,7 +3975,6 @@
      */
     @Nullable
     public final ArrayList<String> createStringArrayList() {
-        assertNotRecycled();
         int N = readInt();
         if (N < 0) {
             return null;
@@ -4202,7 +4001,6 @@
      */
     @Nullable
     public final ArrayList<IBinder> createBinderArrayList() {
-        assertNotRecycled();
         int N = readInt();
         if (N < 0) {
             return null;
@@ -4230,7 +4028,6 @@
     @Nullable
     public final <T extends IInterface> ArrayList<T> createInterfaceArrayList(
             @NonNull Function<IBinder, T> asInterface) {
-        assertNotRecycled();
         int N = readInt();
         if (N < 0) {
             return null;
@@ -4251,7 +4048,6 @@
      * @see #writeStringList
      */
     public final void readStringList(@NonNull List<String> list) {
-        assertNotRecycled();
         int M = list.size();
         int N = readInt();
         int i = 0;
@@ -4273,7 +4069,6 @@
      * @see #writeBinderList
      */
     public final void readBinderList(@NonNull List<IBinder> list) {
-        assertNotRecycled();
         int M = list.size();
         int N = readInt();
         int i = 0;
@@ -4296,7 +4091,6 @@
      */
     public final <T extends IInterface> void readInterfaceList(@NonNull List<T> list,
             @NonNull Function<IBinder, T> asInterface) {
-        assertNotRecycled();
         int M = list.size();
         int N = readInt();
         int i = 0;
@@ -4328,7 +4122,6 @@
     @NonNull
     public final <T extends Parcelable> List<T> readParcelableList(@NonNull List<T> list,
             @Nullable ClassLoader cl) {
-        assertNotRecycled();
         return readParcelableListInternal(list, cl, /*clazz*/ null);
     }
 
@@ -4350,7 +4143,6 @@
     @NonNull
     public <T> List<T> readParcelableList(@NonNull List<T> list,
             @Nullable ClassLoader cl, @NonNull Class<? extends T> clazz) {
-        assertNotRecycled();
         Objects.requireNonNull(list);
         Objects.requireNonNull(clazz);
         return readParcelableListInternal(list, cl, clazz);
@@ -4396,7 +4188,6 @@
      */
     @Nullable
     public final <T> T[] createTypedArray(@NonNull Parcelable.Creator<T> c) {
-        assertNotRecycled();
         int N = readInt();
         if (N < 0) {
             return null;
@@ -4410,7 +4201,6 @@
     }
 
     public final <T> void readTypedArray(@NonNull T[] val, @NonNull Parcelable.Creator<T> c) {
-        assertNotRecycled();
         int N = readInt();
         if (N == val.length) {
             for (int i=0; i<N; i++) {
@@ -4427,7 +4217,6 @@
      */
     @Deprecated
     public final <T> T[] readTypedArray(Parcelable.Creator<T> c) {
-        assertNotRecycled();
         return createTypedArray(c);
     }
 
@@ -4444,7 +4233,6 @@
      */
     @Nullable
     public final <T> T readTypedObject(@NonNull Parcelable.Creator<T> c) {
-        assertNotRecycled();
         if (readInt() != 0) {
             return c.createFromParcel(this);
         } else {
@@ -4471,7 +4259,6 @@
      * @see #readTypedArray
      */
     public <T> void readFixedArray(@NonNull T val) {
-        assertNotRecycled();
         Class<?> componentType = val.getClass().getComponentType();
         if (componentType == boolean.class) {
             readBooleanArray((boolean[]) val);
@@ -4512,7 +4299,6 @@
      */
     public <T, S extends IInterface> void readFixedArray(@NonNull T val,
             @NonNull Function<IBinder, S> asInterface) {
-        assertNotRecycled();
         Class<?> componentType = val.getClass().getComponentType();
         if (IInterface.class.isAssignableFrom(componentType)) {
             readInterfaceArray((S[]) val, asInterface);
@@ -4539,7 +4325,6 @@
      */
     public <T, S extends Parcelable> void readFixedArray(@NonNull T val,
             @NonNull Parcelable.Creator<S> c) {
-        assertNotRecycled();
         Class<?> componentType = val.getClass().getComponentType();
         if (Parcelable.class.isAssignableFrom(componentType)) {
             readTypedArray((S[]) val, c);
@@ -4597,7 +4382,6 @@
      */
     @Nullable
     public <T> T createFixedArray(@NonNull Class<T> cls, @NonNull int... dimensions) {
-        assertNotRecycled();
         // Check if type matches with dimensions
         // If type is one-dimensional array, delegate to other creators
         // Otherwise, create an multi-dimensional array at once and then fill it with readFixedArray
@@ -4671,7 +4455,6 @@
     @Nullable
     public <T, S extends IInterface> T createFixedArray(@NonNull Class<T> cls,
             @NonNull Function<IBinder, S> asInterface, @NonNull int... dimensions) {
-        assertNotRecycled();
         // Check if type matches with dimensions
         // If type is one-dimensional array, delegate to other creators
         // Otherwise, create an multi-dimensional array at once and then fill it with readFixedArray
@@ -4732,7 +4515,6 @@
     @Nullable
     public <T, S extends Parcelable> T createFixedArray(@NonNull Class<T> cls,
             @NonNull Parcelable.Creator<S> c, @NonNull int... dimensions) {
-        assertNotRecycled();
         // Check if type matches with dimensions
         // If type is one-dimensional array, delegate to other creators
         // Otherwise, create an multi-dimensional array at once and then fill it with readFixedArray
@@ -4796,7 +4578,6 @@
      */
     public final <T extends Parcelable> void writeParcelableArray(@Nullable T[] value,
             int parcelableFlags) {
-        assertNotRecycled();
         if (value != null) {
             int N = value.length;
             writeInt(N);
@@ -4815,7 +4596,6 @@
      */
     @Nullable
     public final Object readValue(@Nullable ClassLoader loader) {
-        assertNotRecycled();
         return readValue(loader, /* clazz */ null);
     }
 
@@ -4871,7 +4651,6 @@
      */
     @Nullable
     public Object readLazyValue(@Nullable ClassLoader loader) {
-        assertNotRecycled();
         int start = dataPosition();
         int type = readInt();
         if (isLengthPrefixed(type)) {
@@ -5274,7 +5053,6 @@
     @Deprecated
     @Nullable
     public final <T extends Parcelable> T readParcelable(@Nullable ClassLoader loader) {
-        assertNotRecycled();
         return readParcelableInternal(loader, /* clazz */ null);
     }
 
@@ -5294,7 +5072,6 @@
      */
     @Nullable
     public <T> T readParcelable(@Nullable ClassLoader loader, @NonNull Class<T> clazz) {
-        assertNotRecycled();
         Objects.requireNonNull(clazz);
         return readParcelableInternal(loader, clazz);
     }
@@ -5323,7 +5100,6 @@
     @Nullable
     public final <T extends Parcelable> T readCreator(@NonNull Parcelable.Creator<?> creator,
             @Nullable ClassLoader loader) {
-        assertNotRecycled();
         if (creator instanceof Parcelable.ClassLoaderCreator<?>) {
           Parcelable.ClassLoaderCreator<?> classLoaderCreator =
               (Parcelable.ClassLoaderCreator<?>) creator;
@@ -5351,7 +5127,6 @@
     @Deprecated
     @Nullable
     public final Parcelable.Creator<?> readParcelableCreator(@Nullable ClassLoader loader) {
-        assertNotRecycled();
         return readParcelableCreatorInternal(loader, /* clazz */ null);
     }
 
@@ -5372,7 +5147,6 @@
     @Nullable
     public <T> Parcelable.Creator<T> readParcelableCreator(
             @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
-        assertNotRecycled();
         Objects.requireNonNull(clazz);
         return readParcelableCreatorInternal(loader, clazz);
     }
@@ -5495,7 +5269,6 @@
     @Deprecated
     @Nullable
     public Parcelable[] readParcelableArray(@Nullable ClassLoader loader) {
-        assertNotRecycled();
         return readParcelableArrayInternal(loader, /* clazz */ null);
     }
 
@@ -5516,7 +5289,6 @@
     @SuppressLint({"ArrayReturn", "NullableCollection"})
     @Nullable
     public <T> T[] readParcelableArray(@Nullable ClassLoader loader, @NonNull Class<T> clazz) {
-        assertNotRecycled();
         return readParcelableArrayInternal(loader, requireNonNull(clazz));
     }
 
@@ -5550,7 +5322,6 @@
     @Deprecated
     @Nullable
     public Serializable readSerializable() {
-        assertNotRecycled();
         return readSerializableInternal(/* loader */ null, /* clazz */ null);
     }
 
@@ -5567,7 +5338,6 @@
      */
     @Nullable
     public <T> T readSerializable(@Nullable ClassLoader loader, @NonNull Class<T> clazz) {
-        assertNotRecycled();
         Objects.requireNonNull(clazz);
         return readSerializableInternal(
                 loader == null ? getClass().getClassLoader() : loader, clazz);
@@ -5809,7 +5579,6 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public void readArrayMap(@NonNull ArrayMap<? super String, Object> outVal,
             @Nullable ClassLoader loader) {
-        assertNotRecycled();
         final int N = readInt();
         if (N < 0) {
             return;
@@ -5826,7 +5595,6 @@
      */
     @UnsupportedAppUsage
     public @Nullable ArraySet<? extends Object> readArraySet(@Nullable ClassLoader loader) {
-        assertNotRecycled();
         final int size = readInt();
         if (size < 0) {
             return null;
@@ -5966,7 +5734,6 @@
      * @hide For testing
      */
     public long getOpenAshmemSize() {
-        assertNotRecycled();
         return nativeGetOpenAshmemSize(mNativePtr);
     }
 
diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java
index 4b16c1d..6431f3c 100644
--- a/core/java/android/os/TestLooperManager.java
+++ b/core/java/android/os/TestLooperManager.java
@@ -14,6 +14,8 @@
 
 package android.os;
 
+import android.annotation.FlaggedApi;
+import android.annotation.Nullable;
 import android.util.ArraySet;
 
 import java.util.concurrent.LinkedBlockingQueue;
@@ -93,9 +95,48 @@
     }
 
     /**
-     * Releases the looper to continue standard looping and processing of messages,
-     * no further interactions with TestLooperManager will be allowed after
-     * release() has been called.
+     * Returns the next message that should be executed by this queue, and removes it from the
+     * queue. If the queue is empty or no messages are deliverable, returns null.
+     * This method never blocks.
+     *
+     * <p>Callers should always call {@link #recycle(Message)} on the message when all interactions
+     * with it have completed.
+     */
+    @FlaggedApi(Flags.FLAG_MESSAGE_QUEUE_TESTABILITY)
+    @Nullable
+    public Message pop() {
+        checkReleased();
+        return mQueue.popForTest();
+    }
+
+    /**
+     * Returns the values of {@link Message#when} of the next message that should be executed by
+     * this queue. If the queue is empty or no messages are deliverable, returns null.
+     * This method never blocks.
+     */
+    @FlaggedApi(Flags.FLAG_MESSAGE_QUEUE_TESTABILITY)
+    @SuppressWarnings("AutoBoxing")  // box the primitive long, or return null to indicate no value
+    @Nullable
+    public Long peekWhen() {
+        checkReleased();
+        return mQueue.peekWhenForTest();
+    }
+
+    /**
+     * Checks whether the Looper is currently blocked on a sync barrier.
+     *
+     * A Looper is blocked on a sync barrier if there is a Message in the Looper's
+     * queue that is ready for execution but is behind a sync barrier
+     */
+    @FlaggedApi(Flags.FLAG_MESSAGE_QUEUE_TESTABILITY)
+    public boolean isBlockedOnSyncBarrier() {
+        checkReleased();
+        return mQueue.isBlockedOnSyncBarrier();
+    }
+
+    /**
+     * Releases the looper to continue standard looping and processing of messages, no further
+     * interactions with TestLooperManager will be allowed after release() has been called.
      */
     public void release() {
         synchronized (sHeldLoopers) {
diff --git a/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl b/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl
index c45c51d..af56bfe 100644
--- a/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl
+++ b/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl
@@ -16,7 +16,7 @@
 
 package android.os.instrumentation;
 
-import android.os.instrumentation.ExecutableMethodFileOffsets;
+import android.os.instrumentation.IOffsetCallback;
 import android.os.instrumentation.MethodDescriptor;
 import android.os.instrumentation.TargetProcess;
 
@@ -28,6 +28,7 @@
 interface IDynamicInstrumentationManager {
     /** Provides ART metadata about the described compiled method within the target process */
     @PermissionManuallyEnforced
-    @nullable ExecutableMethodFileOffsets getExecutableMethodFileOffsets(
-            in TargetProcess targetProcess, in MethodDescriptor methodDescriptor);
+    void getExecutableMethodFileOffsets(
+            in TargetProcess targetProcess, in MethodDescriptor methodDescriptor,
+            in IOffsetCallback callback);
 }
diff --git a/core/java/android/os/instrumentation/IOffsetCallback.aidl b/core/java/android/os/instrumentation/IOffsetCallback.aidl
new file mode 100644
index 0000000..a28c93f
--- /dev/null
+++ b/core/java/android/os/instrumentation/IOffsetCallback.aidl
@@ -0,0 +1,28 @@
+/*
+ * 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.os.instrumentation;
+
+import android.os.instrumentation.ExecutableMethodFileOffsets;
+
+/**
+ * System private API for providing dynamic instrumentation offset results.
+ *
+ * {@hide}
+ */
+oneway interface IOffsetCallback {
+    void onResult(in @nullable ExecutableMethodFileOffsets offsets);
+}
diff --git a/core/java/android/os/instrumentation/MethodDescriptorParser.java b/core/java/android/os/instrumentation/MethodDescriptorParser.java
new file mode 100644
index 0000000..57fc44f
--- /dev/null
+++ b/core/java/android/os/instrumentation/MethodDescriptorParser.java
@@ -0,0 +1,82 @@
+/*
+ * 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.os.instrumentation;
+
+import android.annotation.NonNull;
+
+import java.lang.reflect.Method;
+
+/**
+ * A utility class for dynamic instrumentation / uprobestats.
+ *
+ * @hide
+ */
+public final class MethodDescriptorParser {
+
+    /**
+     * Parses a {@link MethodDescriptor} (in string representation) into a {@link Method}.
+     */
+    public static Method parseMethodDescriptor(ClassLoader classLoader,
+            @NonNull MethodDescriptor descriptor) {
+        try {
+            Class<?> javaClass = classLoader.loadClass(descriptor.fullyQualifiedClassName);
+            Class<?>[] parameters = new Class[descriptor.fullyQualifiedParameters.length];
+            for (int i = 0; i < descriptor.fullyQualifiedParameters.length; i++) {
+                String typeName = descriptor.fullyQualifiedParameters[i];
+                boolean isArrayType = typeName.endsWith("[]");
+                if (isArrayType) {
+                    typeName = typeName.substring(0, typeName.length() - 2);
+                }
+                switch (typeName) {
+                    case "boolean":
+                        parameters[i] = isArrayType ? boolean.class.arrayType() : boolean.class;
+                        break;
+                    case "byte":
+                        parameters[i] = isArrayType ? byte.class.arrayType() : byte.class;
+                        break;
+                    case "char":
+                        parameters[i] = isArrayType ? char.class.arrayType() : char.class;
+                        break;
+                    case "short":
+                        parameters[i] = isArrayType ? short.class.arrayType() : short.class;
+                        break;
+                    case "int":
+                        parameters[i] = isArrayType ? int.class.arrayType() : int.class;
+                        break;
+                    case "long":
+                        parameters[i] = isArrayType ? long.class.arrayType() : long.class;
+                        break;
+                    case "float":
+                        parameters[i] = isArrayType ? float.class.arrayType() : float.class;
+                        break;
+                    case "double":
+                        parameters[i] = isArrayType ? double.class.arrayType() : double.class;
+                        break;
+                    default:
+                        parameters[i] = isArrayType ? classLoader.loadClass(typeName).arrayType()
+                                : classLoader.loadClass(typeName);
+                }
+            }
+
+            return javaClass.getDeclaredMethod(descriptor.methodName, parameters);
+        } catch (ClassNotFoundException | NoSuchMethodException e) {
+            throw new IllegalArgumentException(
+                    "The specified method cannot be found. Is this descriptor valid? "
+                            + descriptor, e);
+        }
+    }
+}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index d7750bd..7ad8088 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.graphics.Paint.NEW_FONT_VARIATION_MANAGEMENT;
 import static android.view.ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT;
 import static android.view.ContentInfo.SOURCE_AUTOFILL;
 import static android.view.ContentInfo.SOURCE_CLIPBOARD;
@@ -5542,7 +5543,21 @@
                         && fontVariationSettings.equals(existingSettings))) {
             return true;
         }
-        boolean effective = mTextPaint.setFontVariationSettings(fontVariationSettings);
+
+        final boolean useFontVariationStore = Flags.typefaceRedesignReadonly()
+                && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT);
+        boolean effective;
+        if (useFontVariationStore) {
+            if (mFontWeightAdjustment != 0
+                    && mFontWeightAdjustment != Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED) {
+                mTextPaint.setFontVariationSettings(fontVariationSettings, mFontWeightAdjustment);
+            } else {
+                mTextPaint.setFontVariationSettings(fontVariationSettings);
+            }
+            effective = true;
+        } else {
+            effective = mTextPaint.setFontVariationSettings(fontVariationSettings);
+        }
 
         if (effective && mLayout != null) {
             nullLayouts();
diff --git a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
index b2eeff3..f40cfd9f 100644
--- a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
+++ b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
@@ -532,7 +532,12 @@
     static const size_t kPageSize = getpagesize();
 
     // App compat is only applicable on 16kb-page-size devices.
-    return kPageSize == 0x4000;
+    if (kPageSize != 0x4000) {
+        return false;
+    }
+
+    // Explicit disabled status for app compat
+    return !android::base::GetBoolProperty("pm.16kb.app_compat.disabled", false);
 }
 
 static jint
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index bb76b9f..196da29 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -496,4 +496,8 @@
          not connected state. -->
     <bool name="config_satellite_allow_check_message_in_not_connected">false</bool>
     <java-symbol type="bool" name="config_satellite_allow_check_message_in_not_connected" />
+
+    <!-- Whether to allow TN scanning during satellite session. -->
+    <bool name="config_satellite_allow_tn_scanning_during_satellite_session">true</bool>
+    <java-symbol type="bool" name="config_satellite_allow_tn_scanning_during_satellite_session" />
 </resources>
diff --git a/core/tests/coretests/src/android/os/TestLooperManagerTest.java b/core/tests/coretests/src/android/os/TestLooperManagerTest.java
deleted file mode 100644
index 4d64a3a..0000000
--- a/core/tests/coretests/src/android/os/TestLooperManagerTest.java
+++ /dev/null
@@ -1,91 +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 android.os;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-public class TestLooperManagerTest {
-    private static final String TAG = "TestLooperManagerTest";
-
-    @Rule
-    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
-            .setProvideMainThread(true)
-            .build();
-
-    @Test
-    public void testMainThread() throws Exception {
-        doTest(Looper.getMainLooper());
-    }
-
-    @Test
-    public void testCustomThread() throws Exception {
-        final HandlerThread thread = new HandlerThread(TAG);
-        thread.start();
-        doTest(thread.getLooper());
-    }
-
-    private void doTest(Looper looper) throws Exception {
-        final TestLooperManager tlm =
-                InstrumentationRegistry.getInstrumentation().acquireLooperManager(looper);
-
-        final Handler handler = new Handler(looper);
-        final CountDownLatch latch = new CountDownLatch(1);
-
-        assertFalse(tlm.hasMessages(handler, null, 42));
-
-        handler.sendEmptyMessage(42);
-        handler.post(() -> {
-            latch.countDown();
-        });
-        assertTrue(tlm.hasMessages(handler, null, 42));
-        assertFalse(latch.await(100, TimeUnit.MILLISECONDS));
-
-        final Message first = tlm.next();
-        assertEquals(42, first.what);
-        assertNull(first.callback);
-        tlm.execute(first);
-        assertFalse(tlm.hasMessages(handler, null, 42));
-        assertFalse(latch.await(100, TimeUnit.MILLISECONDS));
-        tlm.recycle(first);
-
-        final Message second = tlm.next();
-        assertNotNull(second.callback);
-        tlm.execute(second);
-        assertFalse(tlm.hasMessages(handler, null, 42));
-        assertTrue(latch.await(100, TimeUnit.MILLISECONDS));
-        tlm.recycle(second);
-
-        tlm.release();
-    }
-}
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index 9bf4d65..2e88514 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -34,6 +34,7 @@
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
 import android.compat.annotation.UnsupportedAppUsage;
+import android.graphics.fonts.FontStyle;
 import android.graphics.fonts.FontVariationAxis;
 import android.graphics.text.TextRunShaper;
 import android.os.Build;
@@ -2141,6 +2142,14 @@
      * @see FontVariationAxis
      */
     public boolean setFontVariationSettings(String fontVariationSettings) {
+        return setFontVariationSettings(fontVariationSettings, 0 /* wght adjust */);
+    }
+
+    /**
+     * Set font variation settings with weight adjustment
+     * @hide
+     */
+    public boolean setFontVariationSettings(String fontVariationSettings, int wghtAdjust) {
         final boolean useFontVariationStore = Flags.typefaceRedesignReadonly()
                 && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT);
         if (useFontVariationStore) {
@@ -2154,8 +2163,13 @@
 
             long builderPtr = nCreateFontVariationBuilder(axes.length);
             for (int i = 0; i < axes.length; ++i) {
-                nAddFontVariationToBuilder(builderPtr, axes[i].getOpenTypeTagValue(),
-                        axes[i].getStyleValue());
+                int tag = axes[i].getOpenTypeTagValue();
+                float value = axes[i].getStyleValue();
+                if (tag == 0x77676874 /* wght */) {
+                    value = Math.clamp(value + wghtAdjust,
+                            FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX);
+                }
+                nAddFontVariationToBuilder(builderPtr, tag, value);
             }
             nSetFontVariationOverride(mNativePaint, builderPtr);
             mFontVariationSettings = fontVariationSettings;
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
index b38d00da..1d0c505 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
@@ -602,8 +602,72 @@
         testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = true)
     }
 
+    @Test
+    fun getExpandedViewContainerPadding_largeScreen_fitsMaxViewWidth() {
+        val expandedViewWidth = context.resources.getDimensionPixelSize(
+            R.dimen.bubble_expanded_view_largescreen_width
+        )
+        // set the screen size so that it is wide enough to fit the maximum width size
+        val screenWidth = expandedViewWidth * 2
+        positioner.update(
+            defaultDeviceConfig.copy(
+                windowBounds = Rect(0, 0, screenWidth, 2000),
+                isLargeScreen = true,
+                isLandscape = false
+            )
+        )
+        val paddings =
+            positioner.getExpandedViewContainerPadding(/* onLeft= */ true, /* isOverflow= */ false)
+
+        val padding = context.resources.getDimensionPixelSize(
+            R.dimen.bubble_expanded_view_largescreen_landscape_padding
+        )
+        val right = screenWidth - expandedViewWidth - padding
+        assertThat(paddings).isEqualTo(intArrayOf(padding - positioner.pointerSize, 0, right, 0))
+    }
+
+    @Test
+    fun getExpandedViewContainerPadding_largeScreen_doesNotFitMaxViewWidth() {
+        positioner.update(
+            defaultDeviceConfig.copy(
+                windowBounds = Rect(0, 0, 600, 2000),
+                isLargeScreen = true,
+                isLandscape = false
+            )
+        )
+        val paddings =
+            positioner.getExpandedViewContainerPadding(/* onLeft= */ true, /* isOverflow= */ false)
+
+        val padding = context.resources.getDimensionPixelSize(
+            R.dimen.bubble_expanded_view_largescreen_landscape_padding
+        )
+        // the screen is not wide enough to fit the maximum width size, so the view fills the screen
+        // minus left and right padding
+        assertThat(paddings).isEqualTo(intArrayOf(padding - positioner.pointerSize, 0, padding, 0))
+    }
+
+    @Test
+    fun getExpandedViewContainerPadding_smallTablet() {
+        val screenWidth = 500
+        positioner.update(
+            defaultDeviceConfig.copy(
+                windowBounds = Rect(0, 0, screenWidth, 2000),
+                isLargeScreen = true,
+                isSmallTablet = true,
+                isLandscape = false
+            )
+        )
+        val paddings =
+            positioner.getExpandedViewContainerPadding(/* onLeft= */ true, /* isOverflow= */ false)
+
+        // for small tablets, the view width is set to be 0.72 * screen width
+        val viewWidth = (screenWidth * 0.72).toInt()
+        val padding = (screenWidth - viewWidth) / 2
+        assertThat(paddings).isEqualTo(intArrayOf(padding - positioner.pointerSize, 0, padding, 0))
+    }
+
     private fun testGetBubbleBarExpandedViewBounds(onLeft: Boolean, isOverflow: Boolean) {
-        positioner.setShowingInBubbleBar(true)
+        positioner.isShowingInBubbleBar = true
         val windowBounds = Rect(0, 0, 2000, 2600)
         val insets = Insets.of(10, 20, 5, 15)
         val deviceConfig =
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index 04c17e5..a5205ee 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -162,6 +162,21 @@
     }
 
     /**
+     * Return the maximum size of the window decoration surface control view host pool, or zero if
+     * there should be no pooling.
+     */
+    public static int getWindowDecorScvhPoolSize(@NonNull Context context) {
+        if (!Flags.enableDesktopWindowingScvhCacheBugFix()) return 0;
+        final int maxTaskLimit = getMaxTaskLimit(context);
+        if (maxTaskLimit > 0) {
+            return maxTaskLimit;
+        }
+        // TODO: b/368032552 - task limit equal to 0 means unlimited. Figure out what the pool
+        //  size should be in that case.
+        return 0;
+    }
+
+    /**
      * Return {@code true} if the current device supports desktop mode.
      */
     @VisibleForTesting
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 673d8b3..60a52a8 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
@@ -214,6 +214,10 @@
                         }
                         ProtoLog.i(WM_SHELL_BACK_PREVIEW, "Navigation window gone.");
                         setTriggerBack(false);
+                        // Trigger close transition if necessary.
+                        if (Flags.migratePredictiveBackTransition()) {
+                            mBackTransitionHandler.onAnimationFinished();
+                        }
                         resetTouchTracker();
                         // Don't wait for animation start
                         mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index 0fd4206..de85d9a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -163,8 +163,11 @@
             mExpandedViewLargeScreenWidth = (int) (bounds.width()
                     * EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT);
         } else {
-            mExpandedViewLargeScreenWidth =
-                    res.getDimensionPixelSize(R.dimen.bubble_expanded_view_largescreen_width);
+            int expandedViewLargeScreenSpacing = res.getDimensionPixelSize(
+                    R.dimen.bubble_expanded_view_largescreen_landscape_padding);
+            mExpandedViewLargeScreenWidth = Math.min(
+                    res.getDimensionPixelSize(R.dimen.bubble_expanded_view_largescreen_width),
+                    bounds.width() - expandedViewLargeScreenSpacing * 2);
         }
         if (mDeviceConfig.isLargeScreen()) {
             if (mDeviceConfig.isSmallTablet()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/BoostExecutor.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/BoostExecutor.kt
new file mode 100644
index 0000000..498d0e4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/BoostExecutor.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.common
+
+import android.os.Looper
+import java.util.concurrent.Executor
+
+/** Executor implementation which can be boosted temporarily to a different thread priority.  */
+interface BoostExecutor : Executor {
+    /**
+     * Requests that the executor is boosted until {@link #resetBoost()} is called.
+     */
+    fun setBoost() {}
+
+    /**
+     * Requests that the executor is not boosted (only resets if there are no other boost requests
+     * in progress).
+     */
+    fun resetBoost() {}
+
+    /**
+     * Returns whether the executor is boosted.
+     */
+    fun isBoosted() : Boolean {
+        return false
+    }
+
+    /**
+     * Returns the looper for this executor.
+     */
+    fun getLooper() : Looper? {
+        return Looper.myLooper()
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java
index 736d954..803f16c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java
@@ -16,15 +16,50 @@
 
 package com.android.wm.shell.common;
 
+import static android.os.Process.THREAD_PRIORITY_DEFAULT;
+import static android.os.Process.setThreadPriority;
+
 import android.annotation.NonNull;
 import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.function.BiConsumer;
 
 /** Executor implementation which is backed by a Handler. */
 public class HandlerExecutor implements ShellExecutor {
+    @NonNull
     private final Handler mHandler;
+    // See android.os.Process#THREAD_PRIORITY_*
+    private final int mDefaultThreadPriority;
+    private final int mBoostedThreadPriority;
+    // Number of current requests to boost thread priority
+    private int mBoostCount;
+    private final Object mBoostLock = new Object();
+    // Default function for setting thread priority (tid, priority)
+    private BiConsumer<Integer, Integer> mSetThreadPriorityFn =
+            HandlerExecutor::setThreadPriorityInternal;
 
     public HandlerExecutor(@NonNull Handler handler) {
+        this(handler, THREAD_PRIORITY_DEFAULT, THREAD_PRIORITY_DEFAULT);
+    }
+
+    /**
+     * Used only if this executor can be boosted, if so, it can be boosted to the given
+     * {@param boostPriority}.
+     */
+    public HandlerExecutor(@NonNull Handler handler, int defaultThreadPriority,
+            int boostedThreadPriority) {
         mHandler = handler;
+        mDefaultThreadPriority = defaultThreadPriority;
+        mBoostedThreadPriority = boostedThreadPriority;
+    }
+
+    @VisibleForTesting
+    void replaceSetThreadPriorityFn(BiConsumer<Integer, Integer> setThreadPriorityFn) {
+        mSetThreadPriorityFn = setThreadPriorityFn;
     }
 
     @Override
@@ -56,9 +91,54 @@
     }
 
     @Override
+    public void setBoost() {
+        synchronized (mBoostLock) {
+            if (mDefaultThreadPriority == mBoostedThreadPriority) {
+                // Nothing to boost
+                return;
+            }
+            if (mBoostCount == 0) {
+                mSetThreadPriorityFn.accept(
+                        ((HandlerThread) mHandler.getLooper().getThread()).getThreadId(),
+                        mBoostedThreadPriority);
+            }
+            mBoostCount++;
+        }
+    }
+
+    @Override
+    public void resetBoost() {
+        synchronized (mBoostLock) {
+            mBoostCount--;
+            if (mBoostCount == 0) {
+                mSetThreadPriorityFn.accept(
+                        ((HandlerThread) mHandler.getLooper().getThread()).getThreadId(),
+                        mDefaultThreadPriority);
+            }
+        }
+    }
+
+    @Override
+    public boolean isBoosted() {
+        synchronized (mBoostLock) {
+            return mBoostCount > 0;
+        }
+    }
+
+    @Override
+    @NonNull
+    public Looper getLooper() {
+        return mHandler.getLooper();
+    }
+
+    @Override
     public void assertCurrentThread() {
         if (!mHandler.getLooper().isCurrentThread()) {
             throw new IllegalStateException("must be called on " + mHandler);
         }
     }
+
+    private static void setThreadPriorityInternal(Integer tid, Integer priority) {
+        setThreadPriority(tid, priority);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java
index 2c2961f..9e5071e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java
@@ -18,15 +18,15 @@
 
 import java.lang.reflect.Array;
 import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Supplier;
 
 /**
  * Super basic Executor interface that adds support for delayed execution and removing callbacks.
- * Intended to wrap Handler while better-supporting testing.
+ * Intended to wrap Handler while better-supporting testing.  Not every ShellExecutor implementation
+ * may support boosting.
  */
-public interface ShellExecutor extends Executor {
+public interface ShellExecutor extends BoostExecutor {
 
     /**
      * Executes the given runnable. If the caller is running on the same looper as this executor,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
index c5644a8..d7ddbde 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
@@ -18,6 +18,7 @@
 
 import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
 import static android.os.Process.THREAD_PRIORITY_DISPLAY;
+import static android.os.Process.THREAD_PRIORITY_FOREGROUND;
 import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST;
 
 import android.content.Context;
@@ -205,13 +206,14 @@
     }
 
     /**
-     * Provides a Shell background thread Executor for low priority background tasks.
+     * Provides a Shell background thread Executor for low priority background tasks.  The thread
+     * may also be boosted to THREAD_PRIORITY_FOREGROUND if necessary.
      */
     @WMSingleton
     @Provides
     @ShellBackgroundThread
     public static ShellExecutor provideSharedBackgroundExecutor(
             @ShellBackgroundThread Handler handler) {
-        return new HandlerExecutor(handler);
+        return new HandlerExecutor(handler, THREAD_PRIORITY_BACKGROUND, THREAD_PRIORITY_FOREGROUND);
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 86e0d08..f9e3be9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -152,6 +152,7 @@
 import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer;
 import com.android.wm.shell.windowdecor.common.viewhost.DefaultWindowDecorViewHostSupplier;
+import com.android.wm.shell.windowdecor.common.viewhost.PooledWindowDecorViewHostSupplier;
 import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost;
 import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier;
 import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromoController;
@@ -347,7 +348,12 @@
     @WMSingleton
     @Provides
     static WindowDecorViewHostSupplier<WindowDecorViewHost> provideWindowDecorViewHostSupplier(
+            @NonNull Context context,
             @ShellMainThread @NonNull CoroutineScope mainScope) {
+        final int poolSize = DesktopModeStatus.getWindowDecorScvhPoolSize(context);
+        if (DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context) && poolSize > 0) {
+            return new PooledWindowDecorViewHostSupplier(mainScope, poolSize);
+        }
         return new DefaultWindowDecorViewHostSupplier(mainScope);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
index 7764688..50187d5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
@@ -77,6 +77,10 @@
     override fun startMinimizedModeTransition(wct: WindowContainerTransaction?): IBinder =
         freeformTaskTransitionHandler.startMinimizedModeTransition(wct)
 
+    /** Delegates starting PiP transition to [FreeformTaskTransitionHandler]. */
+    override fun startPipTransition(wct: WindowContainerTransaction?): IBinder =
+        freeformTaskTransitionHandler.startPipTransition(wct)
+
     /** Starts close transition and handles or delegates desktop task close animation. */
     override fun startRemoveTransition(wct: WindowContainerTransaction?): IBinder {
         if (
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 1ec8684..0bc7ca9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -50,6 +50,7 @@
 import android.view.WindowManager.TRANSIT_CLOSE
 import android.view.WindowManager.TRANSIT_NONE
 import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_PIP
 import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.widget.Toast
 import android.window.DesktopModeFlags
@@ -220,6 +221,7 @@
     // Launch cookie used to identify a drag and drop transition to fullscreen after it has begun.
     // Used to prevent handleRequest from moving the new fullscreen task to freeform.
     private var dragAndDropFullscreenCookie: Binder? = null
+    private var pendingPipTransitionAndTask: Pair<IBinder, Int>? = null
 
     init {
         desktopMode = DesktopModeImpl()
@@ -361,8 +363,15 @@
         }
 
         val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)
-        requireNotNull(tdaInfo) {
-            "This method can only be called with the ID of a display having non-null DisplayArea."
+        // A non-organized display (e.g., non-trusted virtual displays used in CTS) doesn't have
+        // TDA.
+        if (tdaInfo == null) {
+            logW(
+                "forceEnterDesktop cannot find DisplayAreaInfo for displayId=%d. This could happen" +
+                    " when the display is a non-trusted virtual display.",
+                displayId,
+            )
+            return false
         }
         val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
         val isFreeformDisplay = tdaWindowingMode == WINDOWING_MODE_FREEFORM
@@ -557,6 +566,26 @@
     }
 
     fun minimizeTask(taskInfo: RunningTaskInfo) {
+        val wct = WindowContainerTransaction()
+
+        val isMinimizingToPip = taskInfo.pictureInPictureParams?.isAutoEnterEnabled() ?: false
+        // If task is going to PiP, start a PiP transition instead of a minimize transition
+        if (isMinimizingToPip) {
+            val requestInfo = TransitionRequestInfo(
+                TRANSIT_PIP, /* triggerTask= */ null, taskInfo, /* remoteTransition= */ null,
+                /* displayChange= */ null, /* flags= */ 0
+            )
+            val requestRes = transitions.dispatchRequest(Binder(), requestInfo, /* skip= */ null)
+            wct.merge(requestRes.second, true)
+            pendingPipTransitionAndTask =
+                freeformTaskTransitionStarter.startPipTransition(wct) to taskInfo.taskId
+            return
+        }
+
+        minimizeTaskInner(taskInfo)
+    }
+
+    private fun minimizeTaskInner(taskInfo: RunningTaskInfo) {
         val taskId = taskInfo.taskId
         val displayId = taskInfo.displayId
         val wct = WindowContainerTransaction()
@@ -884,7 +913,10 @@
             destinationBounds.height(),
             displayController,
         )
-        toggleResizeDesktopTaskTransitionHandler.startTransition(wct)
+        toggleResizeDesktopTaskTransitionHandler.startTransition(
+            wct,
+            interaction.animationStartBounds,
+        )
     }
 
     private fun dragToMaximizeDesktopTask(
@@ -915,6 +947,7 @@
                 direction = ToggleTaskSizeInteraction.Direction.MAXIMIZE,
                 source = ToggleTaskSizeInteraction.Source.HEADER_DRAG_TO_TOP,
                 inputMethod = DesktopModeEventLogger.getInputMethodFromMotionEvent(motionEvent),
+                animationStartBounds = currentDragBounds,
             ),
         )
     }
@@ -1335,6 +1368,21 @@
         return false
     }
 
+    override fun onTransitionConsumed(
+        transition: IBinder,
+        aborted: Boolean,
+        finishT: Transaction?
+    ) {
+        pendingPipTransitionAndTask?.let { (pipTransition, taskId) ->
+            if (transition == pipTransition) {
+                if (aborted) {
+                    shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { minimizeTaskInner(it) }
+                }
+                pendingPipTransitionAndTask = null
+            }
+        }
+    }
+
     override fun handleRequest(
         transition: IBinder,
         request: TransitionRequestInfo,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/common/ToggleTaskSizeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/common/ToggleTaskSizeUtils.kt
index 7afd8d7..f6ebf72 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/common/ToggleTaskSizeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/common/ToggleTaskSizeUtils.kt
@@ -15,6 +15,7 @@
  */
 package com.android.wm.shell.desktopmode.common
 
+import android.graphics.Rect
 import com.android.internal.jank.Cuj
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
@@ -23,10 +24,13 @@
 import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction.Source
 
 /** Represents a user interaction to toggle a desktop task's size from to maximize or vice versa. */
-data class ToggleTaskSizeInteraction(
+data class ToggleTaskSizeInteraction
+@JvmOverloads
+constructor(
     val direction: Direction,
     val source: Source,
     val inputMethod: InputMethod,
+    val animationStartBounds: Rect? = null,
 ) {
     constructor(
         isMaximized: Boolean,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md
index 9d01535..837a6dd3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md
@@ -36,7 +36,8 @@
   thread)
   - This is always another thread even if config_enableShellMainThread is not set true
   - **Note**:
-    - This thread runs with `THREAD_PRIORITY_BACKGROUND` priority
+    - This thread runs with `THREAD_PRIORITY_BACKGROUND` priority but can be requested to be boosted
+      to `THREAD_PRIORITY_FOREGROUND`
 - `ShellAnimationThread` (currently only used for Transitions and Splitscreen, but potentially all
   animations could be offloaded here)
 - `ShellSplashScreenThread` (only for use with splashscreens)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
index 2ae9828..52b6c62 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
@@ -18,6 +18,7 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.WindowManager.TRANSIT_PIP;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -99,6 +100,12 @@
         return token;
     }
 
+    @Override
+    public IBinder startPipTransition(WindowContainerTransaction wct) {
+        final IBinder token = mTransitions.startTransition(TRANSIT_PIP, wct, null);
+        mPendingTransitionTokens.add(token);
+        return token;
+    }
 
     @Override
     public IBinder startRemoveTransition(WindowContainerTransaction wct) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java
index 5984d48..a874a5b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java
@@ -51,4 +51,13 @@
      * @return the started transition
      */
     IBinder startRemoveTransition(WindowContainerTransaction wct);
+
+    /**
+     * Starts PiP transition
+     *
+     * @param wct the {@link WindowContainerTransaction} that launches the PiP
+     *
+     * @return the started transition
+     */
+    IBinder startPipTransition(WindowContainerTransaction wct);
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 1efe2ff..dae3c21 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -55,6 +55,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.util.Preconditions;
+import com.android.window.flags.Flags;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
@@ -729,6 +730,10 @@
                     && getFixedRotationDelta(info, pipTaskChange) == ROTATION_90) {
                 adjustedSourceRectHint.offset(cutoutInsets.left, cutoutInsets.top);
             }
+            if (Flags.enableDesktopWindowingPip()) {
+                adjustedSourceRectHint.offset(-pipActivityChange.getStartAbsBounds().left,
+                        -pipActivityChange.getStartAbsBounds().top);
+            }
         } else {
             // For non-valid app provided src-rect-hint, calculate one to crop into during
             // app icon overlay animation.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
index 82c0aaf..361d7663 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
@@ -186,6 +186,7 @@
      */
     public void setObscuredTouchRect(Rect obscuredRect) {
         mObscuredTouchRegion = obscuredRect != null ? new Region(obscuredRect) : null;
+        invalidate();
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplier.kt
new file mode 100644
index 0000000..adb0ba6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplier.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.common.viewhost
+
+import android.content.Context
+import android.os.Trace
+import android.util.Pools
+import android.view.Display
+import android.view.SurfaceControl
+import com.android.wm.shell.shared.annotations.ShellMainThread
+import kotlinx.coroutines.CoroutineScope
+
+/**
+ * A [WindowDecorViewHostSupplier] backed by a pool to allow recycling view hosts which may be
+ * expensive to recreate for each new or updated window decoration.
+ *
+ * Callers can obtain a [WindowDecorViewHost] using [acquire], which will return a pooled
+ * object if available, or create a new instance and return it if needed. When finished using a
+ * [WindowDecorViewHost], it must be released using [release] to allow it to be sent back
+ * into the pool and reused later on.
+ */
+class PooledWindowDecorViewHostSupplier(
+    @ShellMainThread private val mainScope: CoroutineScope,
+    maxPoolSize: Int,
+) : WindowDecorViewHostSupplier<WindowDecorViewHost> {
+
+    private val pool: Pools.Pool<WindowDecorViewHost> = Pools.SynchronizedPool(maxPoolSize)
+    private var nextDecorViewHostId = 0
+
+    override fun acquire(context: Context, display: Display): WindowDecorViewHost {
+        val pooledViewHost = pool.acquire()
+        if (pooledViewHost != null) {
+            return pooledViewHost
+        }
+        Trace.beginSection("PooledWindowDecorViewHostSupplier#acquire-newInstance")
+        val newDecorViewHost = newInstance(context, display)
+        Trace.endSection()
+        return newDecorViewHost
+    }
+
+    override fun release(viewHost: WindowDecorViewHost, t: SurfaceControl.Transaction) {
+        val pooled = pool.release(viewHost)
+        if (!pooled) {
+            viewHost.release(t)
+        }
+    }
+
+    private fun newInstance(context: Context, display: Display): ReusableWindowDecorViewHost {
+        // Use a reusable window decor view host, as it allows swapping the entire view hierarchy.
+        return ReusableWindowDecorViewHost(
+            context = context,
+            mainScope = mainScope,
+            display = display,
+            id = nextDecorViewHostId++
+        )
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt
new file mode 100644
index 0000000..bf0b118
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.common.viewhost
+
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Region
+import android.view.Display
+import android.view.SurfaceControl
+import android.view.View
+import android.view.WindowManager
+import android.widget.FrameLayout
+import androidx.tracing.Trace
+import com.android.internal.annotations.VisibleForTesting
+import com.android.wm.shell.shared.annotations.ShellMainThread
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+/**
+ * An implementation of [WindowDecorViewHost] that supports:
+ * 1) Replacing the root [View], meaning [WindowDecorViewHost.updateView] maybe be called with
+ *    different [View] instances. This is useful when reusing [WindowDecorViewHost]s instances for
+ *    vastly different view hierarchies, such as Desktop Windowing's App Handles and App Headers.
+ */
+class ReusableWindowDecorViewHost(
+    private val context: Context,
+    @ShellMainThread private val mainScope: CoroutineScope,
+    display: Display,
+    val id: Int,
+    @VisibleForTesting
+    val viewHostAdapter: SurfaceControlViewHostAdapter =
+        SurfaceControlViewHostAdapter(context, display),
+) : WindowDecorViewHost {
+    @VisibleForTesting val rootView = FrameLayout(context)
+
+    private var currentUpdateJob: Job? = null
+
+    override val surfaceControl: SurfaceControl
+        get() = viewHostAdapter.rootSurface
+
+    override fun updateView(
+        view: View,
+        attrs: WindowManager.LayoutParams,
+        configuration: Configuration,
+        touchableRegion: Region?,
+        onDrawTransaction: SurfaceControl.Transaction?,
+    ) {
+        Trace.beginSection("ReusableWindowDecorViewHost#updateView")
+        clearCurrentUpdateJob()
+        updateViewHost(view, attrs, configuration, touchableRegion, onDrawTransaction)
+        Trace.endSection()
+    }
+
+    override fun updateViewAsync(
+        view: View,
+        attrs: WindowManager.LayoutParams,
+        configuration: Configuration,
+        touchableRegion: Region?,
+    ) {
+        Trace.beginSection("ReusableWindowDecorViewHost#updateViewAsync")
+        clearCurrentUpdateJob()
+        currentUpdateJob =
+            mainScope.launch {
+                updateViewHost(
+                    view,
+                    attrs,
+                    configuration,
+                    touchableRegion,
+                    onDrawTransaction = null,
+                )
+            }
+        Trace.endSection()
+    }
+
+    override fun release(t: SurfaceControl.Transaction) {
+        clearCurrentUpdateJob()
+        viewHostAdapter.release(t)
+    }
+
+    private fun updateViewHost(
+        view: View,
+        attrs: WindowManager.LayoutParams,
+        configuration: Configuration,
+        touchableRegion: Region?,
+        onDrawTransaction: SurfaceControl.Transaction?,
+    ) {
+        Trace.beginSection("ReusableWindowDecorViewHost#updateViewHost")
+        viewHostAdapter.prepareViewHost(configuration, touchableRegion)
+        onDrawTransaction?.let { viewHostAdapter.applyTransactionOnDraw(it) }
+        rootView.removeAllViews()
+        rootView.addView(view)
+        viewHostAdapter.updateView(rootView, attrs)
+        Trace.endSection()
+    }
+
+    private fun clearCurrentUpdateJob() {
+        currentUpdateJob?.cancel()
+        currentUpdateJob = null
+    }
+
+    companion object {
+        private const val TAG = "ReusableWindowDecorViewHost"
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/HandlerExecutorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/HandlerExecutorTest.kt
new file mode 100644
index 0000000..799b48c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/HandlerExecutorTest.kt
@@ -0,0 +1,173 @@
+/*
+ * 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.common
+
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.wm.shell.ShellTestCase
+import com.google.common.truth.Truth.assertThat
+import java.util.function.BiConsumer
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.MockitoSession
+import org.mockito.kotlin.whenever
+
+/**
+ * Tests for HandlerExecutor.
+ *
+ * Build/Install/Run:
+ *  atest WMShellUnitTests:HandlerExecutorTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class HandlerExecutorTest : ShellTestCase() {
+
+    class TestSetThreadPriorityFn : BiConsumer<Int, Int> {
+        var lastSetPriority = UNSET_THREAD_PRIORITY
+            private set
+        var callCount = 0
+            private set
+
+        override fun accept(tid: Int, priority: Int) {
+            lastSetPriority = priority
+            callCount++
+        }
+
+        fun reset() {
+            lastSetPriority = UNSET_THREAD_PRIORITY
+            callCount = 0
+        }
+    }
+
+    val testSetPriorityFn = TestSetThreadPriorityFn()
+
+    @Test
+    fun defaultExecutorDisallowBoost() {
+        val executor = createTestHandlerExecutor()
+
+        executor.setBoost()
+
+        assertThat(executor.isBoosted()).isFalse()
+    }
+
+    @Test
+    fun boostExecutor_resetWhenNotSet_expectNoOp() {
+        val executor = createTestHandlerExecutor(DEFAULT_THREAD_PRIORITY, BOOSTED_THREAD_PRIORITY)
+        val mockSession: MockitoSession = ExtendedMockito.mockitoSession()
+            .mockStatic(android.os.Process::class.java)
+            .startMocking()
+
+        try {
+            // Try to reset and ensure we never try to set the thread priority
+            executor.resetBoost()
+
+            assertThat(testSetPriorityFn.callCount).isEqualTo(0)
+            assertThat(executor.isBoosted()).isFalse()
+        } finally {
+            mockSession.finishMocking()
+        }
+    }
+
+    @Test
+    fun boostExecutor_setResetBoost_expectThreadPriorityUpdated() {
+        val executor = createTestHandlerExecutor(DEFAULT_THREAD_PRIORITY, BOOSTED_THREAD_PRIORITY)
+        val mockSession: MockitoSession = ExtendedMockito.mockitoSession()
+            .mockStatic(android.os.Process::class.java)
+            .startMocking()
+
+        try {
+            // Boost and ensure the boosted thread priority is requested
+            executor.setBoost()
+
+            assertThat(testSetPriorityFn.lastSetPriority).isEqualTo(BOOSTED_THREAD_PRIORITY)
+            assertThat(testSetPriorityFn.callCount).isEqualTo(1)
+            assertThat(executor.isBoosted()).isTrue()
+
+            // Reset and ensure the default thread priority is requested
+            executor.resetBoost()
+
+            assertThat(testSetPriorityFn.lastSetPriority).isEqualTo(DEFAULT_THREAD_PRIORITY)
+            assertThat(testSetPriorityFn.callCount).isEqualTo(2)
+            assertThat(executor.isBoosted()).isFalse()
+        } finally {
+            mockSession.finishMocking()
+        }
+    }
+
+    @Test
+    fun boostExecutor_overlappingBoost_expectResetOnlyWhenNotOverlapping() {
+        val executor = createTestHandlerExecutor(DEFAULT_THREAD_PRIORITY, BOOSTED_THREAD_PRIORITY)
+        val mockSession: MockitoSession = ExtendedMockito.mockitoSession()
+            .mockStatic(android.os.Process::class.java)
+            .startMocking()
+
+        try {
+            // Set and ensure we only update the thread priority once
+            executor.setBoost()
+            executor.setBoost()
+
+            assertThat(testSetPriorityFn.lastSetPriority).isEqualTo(BOOSTED_THREAD_PRIORITY)
+            assertThat(testSetPriorityFn.callCount).isEqualTo(1)
+            assertThat(executor.isBoosted()).isTrue()
+
+            // Reset and ensure we are still boosted and the thread priority doesn't change
+            executor.resetBoost()
+
+            assertThat(testSetPriorityFn.lastSetPriority).isEqualTo(BOOSTED_THREAD_PRIORITY)
+            assertThat(testSetPriorityFn.callCount).isEqualTo(1)
+            assertThat(executor.isBoosted()).isTrue()
+
+            // Reset again and ensure we update the thread priority accordingly
+            executor.resetBoost()
+
+            assertThat(testSetPriorityFn.lastSetPriority).isEqualTo(DEFAULT_THREAD_PRIORITY)
+            assertThat(testSetPriorityFn.callCount).isEqualTo(2)
+            assertThat(executor.isBoosted()).isFalse()
+        } finally {
+            mockSession.finishMocking()
+        }
+    }
+
+    /**
+     * Creates a test handler executor backed by a mocked handler thread.
+     */
+    private fun createTestHandlerExecutor(
+        defaultThreadPriority: Int = DEFAULT_THREAD_PRIORITY,
+        boostedThreadPriority: Int = DEFAULT_THREAD_PRIORITY
+    ) : HandlerExecutor {
+        val handler = mock(Handler::class.java)
+        val looper = mock(Looper::class.java)
+        val thread = mock(HandlerThread::class.java)
+        whenever(handler.looper).thenReturn(looper)
+        whenever(looper.thread).thenReturn(thread)
+        whenever(thread.threadId).thenReturn(1234)
+        val executor = HandlerExecutor(handler, defaultThreadPriority, boostedThreadPriority)
+        executor.replaceSetThreadPriorityFn(testSetPriorityFn)
+        return executor
+    }
+
+    companion object {
+        private const val UNSET_THREAD_PRIORITY = 0
+        private const val DEFAULT_THREAD_PRIORITY = 1
+        private const val BOOSTED_THREAD_PRIORITY = 1000
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 3bee588..7c9494c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -21,6 +21,7 @@
 import android.app.ActivityOptions
 import android.app.KeyguardManager
 import android.app.PendingIntent
+import android.app.PictureInPictureParams
 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
@@ -1724,6 +1725,34 @@
   }
 
   @Test
+  fun onDesktopWindowMinimize_pipTask_autoEnterEnabled_startPipTransition() {
+    val task = setUpPipTask(autoEnterEnabled = true)
+    val handler = mock(TransitionHandler::class.java)
+    whenever(freeformTaskTransitionStarter.startPipTransition(any()))
+      .thenReturn(Binder())
+    whenever(transitions.dispatchRequest(any(), any(), anyOrNull()))
+      .thenReturn(android.util.Pair(handler, WindowContainerTransaction())
+    )
+
+    controller.minimizeTask(task)
+
+    verify(freeformTaskTransitionStarter).startPipTransition(any())
+    verify(freeformTaskTransitionStarter, never()).startMinimizedModeTransition(any())
+  }
+
+  @Test
+  fun onDesktopWindowMinimize_pipTask_autoEnterDisabled_startMinimizeTransition() {
+    val task = setUpPipTask(autoEnterEnabled = false)
+    whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+      .thenReturn(Binder())
+
+    controller.minimizeTask(task)
+
+    verify(freeformTaskTransitionStarter).startMinimizedModeTransition(any())
+    verify(freeformTaskTransitionStarter, never()).startPipTransition(any())
+  }
+
+  @Test
   fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() {
     val task = setUpFreeformTask(active = true)
     val transition = Binder()
@@ -3033,20 +3062,21 @@
       .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR)
 
     // Drag move the task to the top edge
+    val currentDragBounds = Rect(100, 50, 500, 1000)
     spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000))
     spyController.onDragPositioningEnd(
       task,
       mockSurface,
       Point(100, 50), /* position */
       PointF(200f, 300f), /* inputCoordinate */
-      Rect(100, 50, 500, 1000), /* currentDragBounds */
+      currentDragBounds,
       Rect(0, 50, 2000, 2000) /* validDragArea */,
       Rect() /* dragStartBounds */,
       motionEvent,
       desktopWindowDecoration)
 
     // Assert bounds set to stable bounds
-    val wct = getLatestToggleResizeDesktopTaskWct()
+    val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
     assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS)
     // Assert event is properly logged
     verify(desktopModeEventLogger, times(1)).logTaskResizingStarted(
@@ -4228,6 +4258,14 @@
     return task
   }
 
+  private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo {
+    return setUpFreeformTask().apply {
+      pictureInPictureParams = PictureInPictureParams.Builder()
+        .setAutoEnterEnabled(autoEnterEnabled)
+        .build()
+    }
+  }
+
   private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
     val task = createHomeTask(displayId)
     whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplierTest.kt
new file mode 100644
index 0000000..40583f8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/PooledWindowDecorViewHostSupplierTest.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.common.viewhost
+
+import android.content.res.Configuration
+import android.graphics.Region
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import android.view.View
+import android.view.WindowManager
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.util.StubTransaction
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.mock
+
+/**
+ * Tests for [PooledWindowDecorViewHostSupplier].
+ *
+ * Build/Install/Run: atest WMShellUnitTests:PooledWindowDecorViewHostSupplierTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class PooledWindowDecorViewHostSupplierTest : ShellTestCase() {
+
+    private lateinit var supplier: PooledWindowDecorViewHostSupplier
+
+    @Test
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun acquire_poolBelowLimit_caches() = runTest {
+        supplier = createSupplier(maxPoolSize = 5)
+
+        val viewHost = FakeWindowDecorViewHost()
+        supplier.release(viewHost, StubTransaction())
+
+        assertThat(supplier.acquire(context, context.display)).isEqualTo(viewHost)
+    }
+
+    @Test
+    fun release_poolBelowLimit_doesNotReleaseViewHost() = runTest {
+        supplier = createSupplier(maxPoolSize = 5)
+
+        val viewHost = FakeWindowDecorViewHost()
+        val mockT = mock<SurfaceControl.Transaction>()
+        supplier.release(viewHost, mockT)
+
+        assertThat(viewHost.released).isFalse()
+    }
+
+    @Test
+    fun release_poolAtLimit_doesNotCache() = runTest {
+        supplier = createSupplier(maxPoolSize = 1)
+        val viewHost = FakeWindowDecorViewHost()
+        supplier.release(viewHost, StubTransaction()) // Maxes pool.
+
+        val viewHost2 = FakeWindowDecorViewHost()
+        supplier.release(viewHost2, StubTransaction()) // Beyond limit.
+
+        assertThat(supplier.acquire(context, context.display)).isEqualTo(viewHost)
+        // Second one wasn't cached, so the acquired one should've been a new instance.
+        assertThat(supplier.acquire(context, context.display)).isNotEqualTo(viewHost2)
+    }
+
+    @Test
+    fun release_poolAtLimit_releasesViewHost() = runTest {
+        supplier = createSupplier(maxPoolSize = 1)
+        val viewHost = FakeWindowDecorViewHost()
+        supplier.release(viewHost, StubTransaction()) // Maxes pool.
+
+        val viewHost2 = FakeWindowDecorViewHost()
+        val mockT = mock<SurfaceControl.Transaction>()
+        supplier.release(viewHost2, mockT) // Beyond limit.
+
+        // Second one doesn't fit, so it needs to be released.
+        assertThat(viewHost2.released).isTrue()
+    }
+
+    private fun CoroutineScope.createSupplier(maxPoolSize: Int) =
+        PooledWindowDecorViewHostSupplier(this, maxPoolSize)
+
+    private class FakeWindowDecorViewHost : WindowDecorViewHost {
+        var released = false
+            private set
+
+        override val surfaceControl: SurfaceControl
+            get() = SurfaceControl()
+
+        override fun updateView(
+            view: View,
+            attrs: WindowManager.LayoutParams,
+            configuration: Configuration,
+            touchableRegion: Region?,
+            onDrawTransaction: SurfaceControl.Transaction?,
+        ) {}
+
+        override fun updateViewAsync(
+            view: View,
+            attrs: WindowManager.LayoutParams,
+            configuration: Configuration,
+            touchableRegion: Region?,
+        ) {}
+
+        override fun release(t: SurfaceControl.Transaction) {
+            released = true
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt
new file mode 100644
index 0000000..245393a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt
@@ -0,0 +1,170 @@
+/*
+ * 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.common.viewhost
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.SurfaceControl
+import android.view.View
+import android.view.WindowManager
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+
+/**
+ * Tests for [ReusableWindowDecorViewHost].
+ *
+ * Build/Install/Run: atest WMShellUnitTests:ReusableWindowDecorViewHostTest
+ */
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class ReusableWindowDecorViewHostTest : ShellTestCase() {
+
+    @Test
+    fun update_differentView_replacesView() = runTest {
+        val view = View(context)
+        val lp = WindowManager.LayoutParams()
+        val reusableVH = createReusableViewHost()
+        reusableVH.updateView(view, lp, context.resources.configuration, null)
+
+        assertThat(reusableVH.rootView.childCount).isEqualTo(1)
+        assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(view)
+
+        val newView = View(context)
+        val newLp = WindowManager.LayoutParams()
+        reusableVH.updateView(newView, newLp, context.resources.configuration, null)
+
+        assertThat(reusableVH.rootView.childCount).isEqualTo(1)
+        assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(newView)
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun updateView_clearsPendingAsyncJob() = runTest {
+        val reusableVH = createReusableViewHost()
+        val asyncView = View(context)
+        val syncView = View(context)
+        val asyncAttrs = WindowManager.LayoutParams(100, 100)
+        val syncAttrs = WindowManager.LayoutParams(200, 200)
+
+        reusableVH.updateViewAsync(
+            view = asyncView,
+            attrs = asyncAttrs,
+            configuration = context.resources.configuration,
+        )
+
+        // No view host yet, since the coroutine hasn't run.
+        assertThat(reusableVH.viewHostAdapter.isInitialized()).isFalse()
+
+        reusableVH.updateView(
+            view = syncView,
+            attrs = syncAttrs,
+            configuration = context.resources.configuration,
+            onDrawTransaction = null,
+        )
+
+        // Would run coroutine if it hadn't been cancelled.
+        advanceUntilIdle()
+
+        assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
+        // View host view/attrs should match the ones from the sync call.
+        assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(syncView)
+        assertThat(reusableVH.view()!!.layoutParams.width).isEqualTo(syncAttrs.width)
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun updateViewAsync() = runTest {
+        val reusableVH = createReusableViewHost()
+        val view = View(context)
+        val attrs = WindowManager.LayoutParams(100, 100)
+
+        reusableVH.updateViewAsync(
+            view = view,
+            attrs = attrs,
+            configuration = context.resources.configuration,
+        )
+
+        assertThat(reusableVH.viewHostAdapter.isInitialized()).isFalse()
+
+        advanceUntilIdle()
+
+        assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun updateViewAsync_clearsPendingAsyncJob() = runTest {
+        val reusableVH = createReusableViewHost()
+
+        val view = View(context)
+        reusableVH.updateViewAsync(
+            view = view,
+            attrs = WindowManager.LayoutParams(100, 100),
+            configuration = context.resources.configuration,
+        )
+        val otherView = View(context)
+        reusableVH.updateViewAsync(
+            view = otherView,
+            attrs = WindowManager.LayoutParams(100, 100),
+            configuration = context.resources.configuration,
+        )
+
+        advanceUntilIdle()
+
+        assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
+        assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(otherView)
+    }
+
+    @Test
+    fun release() = runTest {
+        val reusableVH = createReusableViewHost()
+
+        val view = View(context)
+        reusableVH.updateView(
+            view = view,
+            attrs = WindowManager.LayoutParams(100, 100),
+            configuration = context.resources.configuration,
+            onDrawTransaction = null,
+        )
+
+        val t = mock(SurfaceControl.Transaction::class.java)
+        reusableVH.release(t)
+
+        verify(reusableVH.viewHostAdapter).release(t)
+    }
+
+    private fun CoroutineScope.createReusableViewHost() =
+        ReusableWindowDecorViewHost(
+            context = context,
+            mainScope = this,
+            display = context.display,
+            id = 1,
+            viewHostAdapter = spy(SurfaceControlViewHostAdapter(context, context.display)),
+        )
+
+    private fun ReusableWindowDecorViewHost.view(): View? = viewHostAdapter.viewHost?.view
+}
diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp
index c735989..d1782b2 100644
--- a/libs/hwui/jni/text/TextShaper.cpp
+++ b/libs/hwui/jni/text/TextShaper.cpp
@@ -225,8 +225,8 @@
 
 constexpr float NO_OVERRIDE = -1;
 
-float findValueFromVariationSettings(const minikin::FontFakery& fakery, minikin::AxisTag tag) {
-    for (const minikin::FontVariation& fv : fakery.variationSettings()) {
+float findValueFromVariationSettings(const minikin::VariationSettings& axes, minikin::AxisTag tag) {
+    for (const minikin::FontVariation& fv : axes) {
         if (fv.axisTag == tag) {
             return fv.value;
         }
@@ -238,8 +238,8 @@
 static jfloat TextShaper_Result_getWeightOverride(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) {
     const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
     if (text_feature::typeface_redesign_readonly()) {
-        float value =
-                findValueFromVariationSettings(layout->layout.getFakery(i), minikin::TAG_wght);
+        float value = findValueFromVariationSettings(layout->layout.typeface(i)->GetAxes(),
+                                                     minikin::TAG_wght);
         return std::isnan(value) ? NO_OVERRIDE : value;
     } else {
         return layout->layout.getFakery(i).wghtAdjustment();
@@ -250,8 +250,8 @@
 static jfloat TextShaper_Result_getItalicOverride(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) {
     const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
     if (text_feature::typeface_redesign_readonly()) {
-        float value =
-                findValueFromVariationSettings(layout->layout.getFakery(i), minikin::TAG_ital);
+        float value = findValueFromVariationSettings(layout->layout.typeface(i)->GetAxes(),
+                                                     minikin::TAG_ital);
         return std::isnan(value) ? NO_OVERRIDE : value;
     } else {
         return layout->layout.getFakery(i).italAdjustment();
diff --git a/media/java/android/media/quality/IMediaQualityManager.aidl b/media/java/android/media/quality/IMediaQualityManager.aidl
index 9daebca..253c2d8 100644
--- a/media/java/android/media/quality/IMediaQualityManager.aidl
+++ b/media/java/android/media/quality/IMediaQualityManager.aidl
@@ -25,51 +25,56 @@
 import android.media.quality.PictureProfile;
 import android.media.quality.SoundProfileHandle;
 import android.media.quality.SoundProfile;
+import android.os.UserHandle;
 
 /**
  * Interface for Media Quality Manager
  * @hide
  */
 interface IMediaQualityManager {
-    PictureProfile createPictureProfile(in PictureProfile pp, int userId);
-    void updatePictureProfile(in String id, in PictureProfile pp, int userId);
-    void removePictureProfile(in String id, int userId);
-    PictureProfile getPictureProfile(in int type, in String name, int userId);
-    List<PictureProfile> getPictureProfilesByPackage(in String packageName, int userId);
-    List<PictureProfile> getAvailablePictureProfiles(int userId);
-    boolean setDefaultPictureProfile(in String id, int userId);
-    List<String> getPictureProfilePackageNames(int userId);
-    List<String> getPictureProfileAllowList(int userId);
-    void setPictureProfileAllowList(in List<String> packages, int userId);
-    List<PictureProfileHandle> getPictureProfileHandle(in String[] id, int userId);
+    PictureProfile createPictureProfile(in PictureProfile pp, in UserHandle user);
+    void updatePictureProfile(in String id, in PictureProfile pp, in UserHandle user);
+    void removePictureProfile(in String id, in UserHandle user);
+    boolean setDefaultPictureProfile(in String id, in UserHandle user);
+    PictureProfile getPictureProfile(
+            in int type, in String name, in boolean includeParams, in UserHandle user);
+    List<PictureProfile> getPictureProfilesByPackage(
+            in String packageName, in boolean includeParams, in UserHandle user);
+    List<PictureProfile> getAvailablePictureProfiles(in boolean includeParams, in UserHandle user);
+    List<String> getPictureProfilePackageNames(in UserHandle user);
+    List<String> getPictureProfileAllowList(in UserHandle user);
+    void setPictureProfileAllowList(in List<String> packages, in UserHandle user);
+    List<PictureProfileHandle> getPictureProfileHandle(in String[] id, in UserHandle user);
 
-    SoundProfile createSoundProfile(in SoundProfile pp, int userId);
-    void updateSoundProfile(in String id, in SoundProfile pp, int userId);
-    void removeSoundProfile(in String id, int userId);
-    SoundProfile getSoundProfile(in int type, in String name, int userId);
-    List<SoundProfile> getSoundProfilesByPackage(in String packageName, int userId);
-    List<SoundProfile> getAvailableSoundProfiles(int userId);
-    boolean setDefaultSoundProfile(in String id, int userId);
-    List<String> getSoundProfilePackageNames(int userId);
-    List<String> getSoundProfileAllowList(int userId);
-    void setSoundProfileAllowList(in List<String> packages, int userId);
-    List<SoundProfileHandle> getSoundProfileHandle(in String[] id, int userId);
+    SoundProfile createSoundProfile(in SoundProfile pp, in UserHandle user);
+    void updateSoundProfile(in String id, in SoundProfile pp, in UserHandle user);
+    void removeSoundProfile(in String id, in UserHandle user);
+    boolean setDefaultSoundProfile(in String id, in UserHandle user);
+    SoundProfile getSoundProfile(
+            in int type, in String name, in boolean includeParams, in UserHandle user);
+    List<SoundProfile> getSoundProfilesByPackage(
+            in String packageName, in boolean includeParams, in UserHandle user);
+    List<SoundProfile> getAvailableSoundProfiles(in boolean includeParams, in UserHandle user);
+    List<String> getSoundProfilePackageNames(in UserHandle user);
+    List<String> getSoundProfileAllowList(in UserHandle user);
+    void setSoundProfileAllowList(in List<String> packages, in UserHandle user);
+    List<SoundProfileHandle> getSoundProfileHandle(in String[] id, in UserHandle user);
 
     void registerPictureProfileCallback(in IPictureProfileCallback cb);
     void registerSoundProfileCallback(in ISoundProfileCallback cb);
     void registerAmbientBacklightCallback(in IAmbientBacklightCallback cb);
 
-    List<ParamCapability> getParamCapabilities(in List<String> names, int userId);
+    List<ParamCapability> getParamCapabilities(in List<String> names, in UserHandle user);
 
-    boolean isSupported(int userId);
-    void setAutoPictureQualityEnabled(in boolean enabled, int userId);
-    boolean isAutoPictureQualityEnabled(int userId);
-    void setSuperResolutionEnabled(in boolean enabled, int userId);
-    boolean isSuperResolutionEnabled(int userId);
-    void setAutoSoundQualityEnabled(in boolean enabled, int userId);
-    boolean isAutoSoundQualityEnabled(int userId);
+    boolean isSupported(in UserHandle user);
+    void setAutoPictureQualityEnabled(in boolean enabled, in UserHandle user);
+    boolean isAutoPictureQualityEnabled(in UserHandle user);
+    void setSuperResolutionEnabled(in boolean enabled, in UserHandle user);
+    boolean isSuperResolutionEnabled(in UserHandle user);
+    void setAutoSoundQualityEnabled(in boolean enabled, in UserHandle user);
+    boolean isAutoSoundQualityEnabled(in UserHandle user);
 
-    void setAmbientBacklightSettings(in AmbientBacklightSettings settings, int userId);
-    void setAmbientBacklightEnabled(in boolean enabled, int userId);
-    boolean isAmbientBacklightEnabled(int userId);
+    void setAmbientBacklightSettings(in AmbientBacklightSettings settings, in UserHandle user);
+    void setAmbientBacklightEnabled(in boolean enabled, in UserHandle user);
+    boolean isAmbientBacklightEnabled(in UserHandle user);
 }
diff --git a/media/java/android/media/quality/IPictureProfileCallback.aidl b/media/java/android/media/quality/IPictureProfileCallback.aidl
index 34aa2b0..7071a16 100644
--- a/media/java/android/media/quality/IPictureProfileCallback.aidl
+++ b/media/java/android/media/quality/IPictureProfileCallback.aidl
@@ -29,5 +29,5 @@
     void onPictureProfileUpdated(in String id, in PictureProfile p);
     void onPictureProfileRemoved(in String id, in PictureProfile p);
     void onParamCapabilitiesChanged(in String id, in List<ParamCapability> caps);
-    void onError(in int err);
+    void onError(in String id, in int err);
 }
diff --git a/media/java/android/media/quality/ISoundProfileCallback.aidl b/media/java/android/media/quality/ISoundProfileCallback.aidl
index 9043757..30bb106 100644
--- a/media/java/android/media/quality/ISoundProfileCallback.aidl
+++ b/media/java/android/media/quality/ISoundProfileCallback.aidl
@@ -29,5 +29,5 @@
     void onSoundProfileUpdated(in String id, in SoundProfile p);
     void onSoundProfileRemoved(in String id, in SoundProfile p);
     void onParamCapabilitiesChanged(in String id, in List<ParamCapability> caps);
-    void onError(in int err);
+    void onError(in String id, in int err);
 }
diff --git a/media/java/android/media/quality/MediaQualityManager.java b/media/java/android/media/quality/MediaQualityManager.java
index 024b470c..7e87462 100644
--- a/media/java/android/media/quality/MediaQualityManager.java
+++ b/media/java/android/media/quality/MediaQualityManager.java
@@ -26,6 +26,7 @@
 import android.content.Context;
 import android.media.tv.flags.Flags;
 import android.os.RemoteException;
+import android.os.UserHandle;
 
 import androidx.annotation.RequiresPermission;
 
@@ -48,7 +49,7 @@
 
     private final IMediaQualityManager mService;
     private final Context mContext;
-    private final int mUserId;
+    private final UserHandle mUserHandle;
     private final Object mLock = new Object();
     // @GuardedBy("mLock")
     private final List<PictureProfileCallbackRecord> mPpCallbackRecords = new ArrayList<>();
@@ -66,7 +67,7 @@
      */
     public MediaQualityManager(Context context, IMediaQualityManager service) {
         mContext = context;
-        mUserId = context.getUserId();
+        mUserHandle = context.getUser();
         mService = service;
         IPictureProfileCallback ppCallback = new IPictureProfileCallback.Stub() {
             @Override
@@ -106,11 +107,11 @@
                 }
             }
             @Override
-            public void onError(int err) {
+            public void onError(String profileId, int err) {
                 synchronized (mLock) {
                     for (PictureProfileCallbackRecord record : mPpCallbackRecords) {
                         // TODO: filter callback record
-                        record.postError(err);
+                        record.postError(profileId, err);
                     }
                 }
             }
@@ -153,11 +154,11 @@
                 }
             }
             @Override
-            public void onError(int err) {
+            public void onError(String profileId, int err) {
                 synchronized (mLock) {
                     for (SoundProfileCallbackRecord record : mSpCallbackRecords) {
                         // TODO: filter callback record
-                        record.postError(err);
+                        record.postError(profileId, err);
                     }
                 }
             }
@@ -214,18 +215,21 @@
         }
     }
 
-
     /**
      * Gets picture profile by given profile type and name.
      *
+     * @param type the type of the profile.
+     * @param name the name of the profile.
+     * @param includeParams {@code true} to include parameters in the profile; {@code false}
+     *                      otherwise.
      * @return the corresponding picture profile if available; {@code null} if the name doesn't
-     *         exist.
+     * exist.
      */
     @Nullable
     public PictureProfile getPictureProfile(
-            @PictureProfile.ProfileType int type, @NonNull String name) {
+            @PictureProfile.ProfileType int type, @NonNull String name, boolean includeParams) {
         try {
-            return mService.getPictureProfile(type, name, mUserId);
+            return mService.getPictureProfile(type, name, includeParams, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -235,14 +239,18 @@
     /**
      * Gets profiles that available to the given package.
      *
+     * @param packageName the package name of the profiles.
+     * @param includeParams {@code true} to include parameters in the profile; {@code false}
+     *                      otherwise.
      * @hide
      */
     @SystemApi
     @NonNull
     @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
-    public List<PictureProfile> getPictureProfilesByPackage(@NonNull String packageName) {
+    public List<PictureProfile> getPictureProfilesByPackage(
+            @NonNull String packageName, boolean includeParams) {
         try {
-            return mService.getPictureProfilesByPackage(packageName, mUserId);
+            return mService.getPictureProfilesByPackage(packageName, includeParams, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -250,11 +258,16 @@
 
     /**
      * Gets profiles that available to the caller.
+     *
+     * @param includeParams {@code true} to include parameters in the profile; {@code false}
+     *                      otherwise.
+     * @return the corresponding picture profile if available; {@code null} if the name doesn't
+     * exist.
      */
     @NonNull
-    public List<PictureProfile> getAvailablePictureProfiles() {
+    public List<PictureProfile> getAvailablePictureProfiles(boolean includeParams) {
         try {
-            return mService.getAvailablePictureProfiles(mUserId);
+            return mService.getAvailablePictureProfiles(includeParams, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -272,7 +285,7 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
     public boolean setDefaultPictureProfile(@Nullable String id) {
         try {
-            return mService.setDefaultPictureProfile(id, mUserId);
+            return mService.setDefaultPictureProfile(id, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -281,7 +294,7 @@
     /**
      * Gets all package names whose picture profiles are available.
      *
-     * @see #getPictureProfilesByPackage(String)
+     * @see #getPictureProfilesByPackage(String, boolean)
      * @hide
      */
     @SystemApi
@@ -289,7 +302,7 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
     public List<String> getPictureProfilePackageNames() {
         try {
-            return mService.getPictureProfilePackageNames(mUserId);
+            return mService.getPictureProfilePackageNames(mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -301,7 +314,7 @@
      */
     public List<PictureProfileHandle> getPictureProfileHandle(String[] id) {
         try {
-            return mService.getPictureProfileHandle(id, mUserId);
+            return mService.getPictureProfileHandle(id, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -313,7 +326,7 @@
      */
     public List<SoundProfileHandle> getSoundProfileHandle(String[] id) {
         try {
-            return mService.getSoundProfileHandle(id, mUserId);
+            return mService.getSoundProfileHandle(id, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -324,10 +337,12 @@
      *
      * <p>If the profile is created successfully,
      * {@link PictureProfileCallback#onPictureProfileAdded(String, PictureProfile)} is invoked.
+     *
+     * @param pp the {@link PictureProfile} object to be created.
      */
     public void createPictureProfile(@NonNull PictureProfile pp) {
         try {
-            mService.createPictureProfile(pp, mUserId);
+            mService.createPictureProfile(pp, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -336,10 +351,13 @@
 
     /**
      * Updates an existing picture profile and store it in the system.
+     *
+     * @param profileId the id of the object to be updated.
+     * @param pp the {@link PictureProfile} object to be updated.
      */
     public void updatePictureProfile(@NonNull String profileId, @NonNull PictureProfile pp) {
         try {
-            mService.updatePictureProfile(profileId, pp, mUserId);
+            mService.updatePictureProfile(profileId, pp, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -348,10 +366,12 @@
 
     /**
      * Removes a picture profile from the system.
+     *
+     * @param profileId the id of the object to be removed.
      */
     public void removePictureProfile(@NonNull String profileId) {
         try {
-            mService.removePictureProfile(profileId, mUserId);
+            mService.removePictureProfile(profileId, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -387,18 +407,20 @@
         }
     }
 
-
     /**
      * Gets sound profile by given profile type and name.
      *
-     * @return the corresponding sound profile if available; {@code null} if the name doesn't
-     *         exist.
+     * @param type the type of the profile.
+     * @param name the name of the profile.
+     * @param includeParams {@code true} to include parameters in the profile; {@code false}
+     *                      otherwise.
+     * @return the corresponding sound profile if available; {@code null} if the name doesn't exist.
      */
     @Nullable
     public SoundProfile getSoundProfile(
-            @SoundProfile.ProfileType int type, @NonNull String name) {
+            @SoundProfile.ProfileType int type, @NonNull String name, boolean includeParams) {
         try {
-            return mService.getSoundProfile(type, name, mUserId);
+            return mService.getSoundProfile(type, name, includeParams, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -408,14 +430,18 @@
     /**
      * Gets profiles that available to the given package.
      *
+     * @param packageName the package name of the profiles.
+     * @param includeParams {@code true} to include parameters in the profile; {@code false}
+     *                      otherwise.
      * @hide
      */
     @SystemApi
     @NonNull
     @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE)
-    public List<SoundProfile> getSoundProfilesByPackage(@NonNull String packageName) {
+    public List<SoundProfile> getSoundProfilesByPackage(
+            @NonNull String packageName, boolean includeParams) {
         try {
-            return mService.getSoundProfilesByPackage(packageName, mUserId);
+            return mService.getSoundProfilesByPackage(packageName, includeParams, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -423,11 +449,16 @@
 
     /**
      * Gets profiles that available to the caller package.
+     *
+     * @param includeParams {@code true} to include parameters in the profile; {@code false}
+     *                      otherwise.
+     *
+     * @return the corresponding sound profile if available; {@code null} if the none available.
      */
     @NonNull
-    public List<SoundProfile> getAvailableSoundProfiles() {
+    public List<SoundProfile> getAvailableSoundProfiles(boolean includeParams) {
         try {
-            return mService.getAvailableSoundProfiles(mUserId);
+            return mService.getAvailableSoundProfiles(includeParams, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -445,7 +476,7 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE)
     public boolean setDefaultSoundProfile(@Nullable String id) {
         try {
-            return mService.setDefaultSoundProfile(id, mUserId);
+            return mService.setDefaultSoundProfile(id, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -454,7 +485,7 @@
     /**
      * Gets all package names whose sound profiles are available.
      *
-     * @see #getSoundProfilesByPackage(String)
+     * @see #getSoundProfilesByPackage(String, boolean)
      *
      * @hide
      */
@@ -463,7 +494,7 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE)
     public List<String> getSoundProfilePackageNames() {
         try {
-            return mService.getSoundProfilePackageNames(mUserId);
+            return mService.getSoundProfilePackageNames(mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -475,10 +506,12 @@
      *
      * <p>If the profile is created successfully,
      * {@link SoundProfileCallback#onSoundProfileAdded(String, SoundProfile)} is invoked.
+     *
+     * @param sp the {@link SoundProfile} object to be created.
      */
     public void createSoundProfile(@NonNull SoundProfile sp) {
         try {
-            mService.createSoundProfile(sp, mUserId);
+            mService.createSoundProfile(sp, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -487,10 +520,13 @@
 
     /**
      * Updates an existing sound profile and store it in the system.
+     *
+     * @param profileId the id of the object to be updated.
+     * @param sp the {@link SoundProfile} object to be updated.
      */
     public void updateSoundProfile(@NonNull String profileId, @NonNull SoundProfile sp) {
         try {
-            mService.updateSoundProfile(profileId, sp, mUserId);
+            mService.updateSoundProfile(profileId, sp, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -499,10 +535,12 @@
 
     /**
      * Removes a sound profile from the system.
+     *
+     * @param profileId the id of the object to be removed.
      */
     public void removeSoundProfile(@NonNull String profileId) {
         try {
-            mService.removeSoundProfile(profileId, mUserId);
+            mService.removeSoundProfile(profileId, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -514,7 +552,7 @@
     @NonNull
     public List<ParamCapability> getParamCapabilities(@NonNull List<String> names) {
         try {
-            return mService.getParamCapabilities(names, mUserId);
+            return mService.getParamCapabilities(names, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -532,7 +570,7 @@
     @NonNull
     public List<String> getPictureProfileAllowList() {
         try {
-            return mService.getPictureProfileAllowList(mUserId);
+            return mService.getPictureProfileAllowList(mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -546,7 +584,7 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
     public void setPictureProfileAllowList(@NonNull List<String> packageNames) {
         try {
-            mService.setPictureProfileAllowList(packageNames, mUserId);
+            mService.setPictureProfileAllowList(packageNames, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -564,7 +602,7 @@
     @NonNull
     public List<String> getSoundProfileAllowList() {
         try {
-            return mService.getSoundProfileAllowList(mUserId);
+            return mService.getSoundProfileAllowList(mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -578,7 +616,7 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE)
     public void setSoundProfileAllowList(@NonNull List<String> packageNames) {
         try {
-            mService.setSoundProfileAllowList(packageNames, mUserId);
+            mService.setSoundProfileAllowList(packageNames, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -590,7 +628,7 @@
      */
     public boolean isSupported() {
         try {
-            return mService.isSupported(mUserId);
+            return mService.isSupported(mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -608,7 +646,7 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
     public void setAutoPictureQualityEnabled(boolean enabled) {
         try {
-            mService.setAutoPictureQualityEnabled(enabled, mUserId);
+            mService.setAutoPictureQualityEnabled(enabled, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -619,7 +657,7 @@
      */
     public boolean isAutoPictureQualityEnabled() {
         try {
-            return mService.isAutoPictureQualityEnabled(mUserId);
+            return mService.isAutoPictureQualityEnabled(mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -636,7 +674,7 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
     public void setSuperResolutionEnabled(boolean enabled) {
         try {
-            mService.setSuperResolutionEnabled(enabled, mUserId);
+            mService.setSuperResolutionEnabled(enabled, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -647,7 +685,7 @@
      */
     public boolean isSuperResolutionEnabled() {
         try {
-            return mService.isSuperResolutionEnabled(mUserId);
+            return mService.isSuperResolutionEnabled(mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -665,7 +703,7 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE)
     public void setAutoSoundQualityEnabled(boolean enabled) {
         try {
-            mService.setAutoSoundQualityEnabled(enabled, mUserId);
+            mService.setAutoSoundQualityEnabled(enabled, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -676,7 +714,7 @@
      */
     public boolean isAutoSoundQualityEnabled() {
         try {
-            return mService.isAutoSoundQualityEnabled(mUserId);
+            return mService.isAutoSoundQualityEnabled(mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -725,7 +763,7 @@
             @NonNull AmbientBacklightSettings settings) {
         Preconditions.checkNotNull(settings);
         try {
-            mService.setAmbientBacklightSettings(settings, mUserId);
+            mService.setAmbientBacklightSettings(settings, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -736,7 +774,7 @@
      */
     public boolean isAmbientBacklightEnabled() {
         try {
-            return mService.isAmbientBacklightEnabled(mUserId);
+            return mService.isAmbientBacklightEnabled(mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -750,7 +788,7 @@
     @RequiresPermission(android.Manifest.permission.READ_COLOR_ZONES)
     public void setAmbientBacklightEnabled(boolean enabled) {
         try {
-            mService.setAmbientBacklightEnabled(enabled, mUserId);
+            mService.setAmbientBacklightEnabled(enabled, mUserHandle);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -807,11 +845,11 @@
             });
         }
 
-        public void postError(int error) {
+        public void postError(String profileId, int error) {
             mExecutor.execute(new Runnable() {
                 @Override
                 public void run() {
-                    mCallback.onError(error);
+                    mCallback.onError(profileId, error);
                 }
             });
         }
@@ -867,11 +905,11 @@
             });
         }
 
-        public void postError(int error) {
+        public void postError(String profileId, int error) {
             mExecutor.execute(new Runnable() {
                 @Override
                 public void run() {
-                    mCallback.onError(error);
+                    mCallback.onError(profileId, error);
                 }
             });
         }
@@ -937,9 +975,11 @@
         /**
          * This is invoked when an issue has occurred.
          *
+         * @param profileId the profile ID related to the error. {@code null} if there is no
+         *                  associated profile.
          * @param errorCode the error code
          */
-        public void onError(@PictureProfile.ErrorCode int errorCode) {
+        public void onError(@Nullable String profileId, @PictureProfile.ErrorCode int errorCode) {
         }
 
         /**
@@ -992,9 +1032,11 @@
         /**
          * This is invoked when an issue has occurred.
          *
+         * @param profileId the profile ID related to the error. {@code null} if there is no
+         *                  associated profile.
          * @param errorCode the error code
          */
-        public void onError(@SoundProfile.ErrorCode int errorCode) {
+        public void onError(@Nullable String profileId, @SoundProfile.ErrorCode int errorCode) {
         }
 
         /**
diff --git a/native/android/dynamic_instrumentation_manager.cpp b/native/android/dynamic_instrumentation_manager.cpp
index 5322136..0749731 100644
--- a/native/android/dynamic_instrumentation_manager.cpp
+++ b/native/android/dynamic_instrumentation_manager.cpp
@@ -15,7 +15,9 @@
  */
 
 #define LOG_TAG "ADynamicInstrumentationManager"
+#include <android-base/properties.h>
 #include <android/dynamic_instrumentation_manager.h>
+#include <android/os/instrumentation/BnOffsetCallback.h>
 #include <android/os/instrumentation/ExecutableMethodFileOffsets.h>
 #include <android/os/instrumentation/IDynamicInstrumentationManager.h>
 #include <android/os/instrumentation/MethodDescriptor.h>
@@ -23,7 +25,9 @@
 #include <binder/Binder.h>
 #include <binder/IServiceManager.h>
 #include <utils/Log.h>
+#include <utils/StrongPointer.h>
 
+#include <future>
 #include <mutex>
 #include <optional>
 #include <string>
@@ -31,6 +35,9 @@
 
 namespace android::dynamicinstrumentationmanager {
 
+using android::os::instrumentation::BnOffsetCallback;
+using android::os::instrumentation::ExecutableMethodFileOffsets;
+
 // Global instance of IDynamicInstrumentationManager, service is obtained only on first use.
 static std::mutex mLock;
 static sp<os::instrumentation::IDynamicInstrumentationManager> mService;
@@ -131,6 +138,30 @@
     delete instance;
 }
 
+class ResultCallback : public BnOffsetCallback {
+public:
+    ::android::binder::Status onResult(
+            const ::std::optional<ExecutableMethodFileOffsets>& offsets) override {
+        promise_.set_value(offsets);
+        return android::binder::Status::ok();
+    }
+
+    std::optional<ExecutableMethodFileOffsets> waitForResult() {
+        std::future<std::optional<ExecutableMethodFileOffsets>> futureResult =
+                promise_.get_future();
+        auto futureStatus = futureResult.wait_for(
+                std::chrono::seconds(1 * android::base::HwTimeoutMultiplier()));
+        if (futureStatus == std::future_status::ready) {
+            return futureResult.get();
+        } else {
+            return std::nullopt;
+        }
+    }
+
+private:
+    std::promise<std::optional<ExecutableMethodFileOffsets>> promise_;
+};
+
 int32_t ADynamicInstrumentationManager_getExecutableMethodFileOffsets(
         const ADynamicInstrumentationManager_TargetProcess* targetProcess,
         const ADynamicInstrumentationManager_MethodDescriptor* methodDescriptor,
@@ -150,15 +181,15 @@
         return INVALID_OPERATION;
     }
 
-    std::optional<android::os::instrumentation::ExecutableMethodFileOffsets> offsets;
+    android::sp<ResultCallback> resultCallback = android::sp<ResultCallback>::make();
     binder_status_t result =
             service->getExecutableMethodFileOffsets(targetProcessParcel, methodDescriptorParcel,
-                                                    &offsets)
+                                                    resultCallback)
                     .exceptionCode();
     if (result != OK) {
         return result;
     }
-
+    std::optional<ExecutableMethodFileOffsets> offsets = resultCallback->waitForResult();
     if (offsets != std::nullopt) {
         auto* value = new ADynamicInstrumentationManager_ExecutableMethodFileOffsets();
         value->containerPath = offsets->containerPath;
@@ -170,4 +201,4 @@
     }
 
     return result;
-}
\ No newline at end of file
+}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
index e12c7a2..326bff4 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -201,6 +201,16 @@
         "could_not_read_from_cursor";
     private static final String ERROR_FAILED_TO_WRITE_ENTITY =
         "failed_to_write_entity";
+    private static final String ERROR_COULD_NOT_READ_ENTITY =
+        "could_not_read_entity";
+    private static final String ERROR_SKIPPED_BY_SYSTEM = "skipped_by_system";
+    private static final String ERROR_SKIPPED_BY_BLOCKLIST =
+        "skipped_by_dynamic_blocklist";
+    private static final String ERROR_SKIPPED_PRESERVED = "skipped_preserved";
+    private static final String ERROR_SKIPPED_DUE_TO_LARGE_SCREEN =
+        "skipped_due_to_large_screen";
+    private static final String ERROR_DID_NOT_PASS_VALIDATION = "did_not_pass_validation";
+
 
     // Name of the temporary file we use during full backup/restore.  This is
     // stored in the full-backup tarfile as well, so should not be changed.
@@ -373,7 +383,7 @@
                     restoreSettings(data, Settings.System.CONTENT_URI, movedToGlobal,
                             movedToSecure, /* movedToSystem= */ null,
                             R.array.restore_blocked_system_settings, dynamicBlockList,
-                            preservedSystemSettings);
+                            preservedSystemSettings, KEY_SYSTEM);
                     mSettingsHelper.applyAudioSettings();
                     break;
 
@@ -381,13 +391,13 @@
                     restoreSettings(data, Settings.Secure.CONTENT_URI, movedToGlobal,
                             /* movedToSecure= */ null, movedToSystem,
                             R.array.restore_blocked_secure_settings, dynamicBlockList,
-                            preservedSecureSettings);
+                            preservedSecureSettings, KEY_SECURE);
                     break;
 
                 case KEY_GLOBAL :
                     restoreSettings(data, Settings.Global.CONTENT_URI, /* movedToGlobal= */ null,
                             movedToSecure, movedToSystem, R.array.restore_blocked_global_settings,
-                            dynamicBlockList, preservedGlobalSettings);
+                            dynamicBlockList, preservedGlobalSettings, KEY_GLOBAL);
                     break;
 
                 case KEY_WIFI_SUPPLICANT :
@@ -506,7 +516,7 @@
             restoreSettings(buffer, nBytes, Settings.System.CONTENT_URI, movedToGlobal,
                     movedToSecure, /* movedToSystem= */ null,
                     R.array.restore_blocked_system_settings, Collections.emptySet(),
-                    Collections.emptySet());
+                    Collections.emptySet(), KEY_SYSTEM);
 
             // secure settings
             nBytes = in.readInt();
@@ -516,7 +526,7 @@
             restoreSettings(buffer, nBytes, Settings.Secure.CONTENT_URI, movedToGlobal,
                     /* movedToSecure= */ null, movedToSystem,
                     R.array.restore_blocked_secure_settings, Collections.emptySet(),
-                    Collections.emptySet());
+                    Collections.emptySet(), KEY_SECURE);
 
             // Global only if sufficiently new
             if (version >= FULL_BACKUP_ADDED_GLOBAL) {
@@ -527,7 +537,7 @@
                 restoreSettings(buffer, nBytes, Settings.Global.CONTENT_URI,
                         /* movedToGlobal= */ null, movedToSecure, movedToSystem,
                         R.array.restore_blocked_global_settings, Collections.emptySet(),
-                        Collections.emptySet());
+                        Collections.emptySet(), KEY_GLOBAL);
             }
 
             // locale
@@ -808,7 +818,8 @@
         return baos.toByteArray();
     }
 
-    private void restoreSettings(
+    @VisibleForTesting
+    void restoreSettings(
             BackupDataInput data,
             Uri contentUri,
             Set<String> movedToGlobal,
@@ -816,12 +827,17 @@
             Set<String> movedToSystem,
             int blockedSettingsArrayId,
             Set<String> dynamicBlockList,
-            Set<String> settingsToPreserve) {
+            Set<String> settingsToPreserve,
+            String settingsKey) {
         byte[] settings = new byte[data.getDataSize()];
         try {
             data.readEntityData(settings, 0, settings.length);
         } catch (IOException ioe) {
             Log.e(TAG, "Couldn't read entity data");
+            if (areAgentMetricsEnabled) {
+                mBackupRestoreEventLogger.logItemsRestoreFailed(
+                    settingsKey, /* count= */ 1, ERROR_COULD_NOT_READ_ENTITY);
+            }
             return;
         }
         restoreSettings(
@@ -833,7 +849,8 @@
                 movedToSystem,
                 blockedSettingsArrayId,
                 dynamicBlockList,
-                settingsToPreserve);
+                settingsToPreserve,
+                settingsKey);
     }
 
     private void restoreSettings(
@@ -845,7 +862,8 @@
             Set<String> movedToSystem,
             int blockedSettingsArrayId,
             Set<String> dynamicBlockList,
-            Set<String> settingsToPreserve) {
+            Set<String> settingsToPreserve,
+            String settingsKey) {
         restoreSettings(
                 settings,
                 0,
@@ -856,7 +874,8 @@
                 movedToSystem,
                 blockedSettingsArrayId,
                 dynamicBlockList,
-                settingsToPreserve);
+                settingsToPreserve,
+                settingsKey);
     }
 
     @VisibleForTesting
@@ -870,12 +889,13 @@
             Set<String> movedToSystem,
             int blockedSettingsArrayId,
             Set<String> dynamicBlockList,
-            Set<String> settingsToPreserve) {
+            Set<String> settingsToPreserve,
+            String settingsKey) {
         if (DEBUG) {
             Log.i(TAG, "restoreSettings: " + contentUri);
         }
 
-        SettingsBackupWhitelist whitelist = getBackupWhitelist(contentUri);
+        SettingsBackupAllowlist allowlist = getBackupAllowlist(contentUri);
 
         // Restore only the white list data.
         final ArrayMap<String, String> cachedEntries = new ArrayMap<>();
@@ -885,7 +905,8 @@
 
         Set<String> blockedSettings = getBlockedSettings(blockedSettingsArrayId);
 
-        for (String key : whitelist.mSettingsWhitelist) {
+        int restoredSettingsCount = 0;
+        for (String key : allowlist.mSettingsAllowlist) {
             boolean isBlockedBySystem = blockedSettings != null && blockedSettings.contains(key);
             if (isBlockedBySystem || isBlockedByDynamicList(dynamicBlockList, contentUri,  key)) {
                 Log.i(
@@ -895,6 +916,12 @@
                                 + " removed from restore by "
                                 + (isBlockedBySystem ? "system" : "dynamic")
                                 + " block list");
+                if (areAgentMetricsEnabled) {
+                    mBackupRestoreEventLogger.logItemsRestoreFailed(
+                        settingsKey,
+                        /* count= */ 1,
+                        isBlockedBySystem ? ERROR_SKIPPED_BY_SYSTEM : ERROR_SKIPPED_BY_BLOCKLIST);
+                }
                 continue;
             }
 
@@ -905,12 +932,20 @@
             if (isSettingPreserved && !Settings.Secure.NAVIGATION_MODE.equals(key)) {
                 Log.i(TAG, "Skipping restore for setting " + key + " as it is marked as "
                         + "preserved");
+                if (areAgentMetricsEnabled) {
+                    mBackupRestoreEventLogger.logItemsRestoreFailed(
+                            settingsKey, /* count= */ 1, ERROR_SKIPPED_PRESERVED);
+                }
                 continue;
             }
 
             if (LargeScreenSettings.doNotRestoreIfLargeScreenSetting(key, getBaseContext())) {
                 Log.i(TAG, "Skipping restore for setting " + key + " as the target device "
                         + "is a large screen (i.e tablet or foldable in unfolded state)");
+                if (areAgentMetricsEnabled) {
+                    mBackupRestoreEventLogger.logItemsRestoreFailed(
+                            settingsKey, /* count= */ 1, ERROR_SKIPPED_DUE_TO_LARGE_SCREEN);
+                }
                 continue;
             }
 
@@ -947,19 +982,34 @@
             }
 
             // only restore the settings that have valid values
-            if (!isValidSettingValue(key, value, whitelist.mSettingsValidators)) {
+            if (!isValidSettingValue(key, value, allowlist.mSettingsValidators)) {
                 Log.w(TAG, "Attempted restore of " + key + " setting, but its value didn't pass"
                         + " validation, value: " + value);
+                if (areAgentMetricsEnabled) {
+                    mBackupRestoreEventLogger.logItemsRestoreFailed(
+                            settingsKey, /* count= */ 1, ERROR_DID_NOT_PASS_VALIDATION);
+                }
                 continue;
             }
 
             final Uri destination;
+            // If the destination changes, we need to update the key used as datatype for metrics.
+            String finalSettingsKey = settingsKey;
             if (movedToGlobal != null && movedToGlobal.contains(key)) {
                 destination = Settings.Global.CONTENT_URI;
+                if (areAgentMetricsEnabled) {
+                    finalSettingsKey = KEY_GLOBAL;
+                }
             } else if (movedToSecure != null && movedToSecure.contains(key)) {
                 destination = Settings.Secure.CONTENT_URI;
+                if (areAgentMetricsEnabled) {
+                    finalSettingsKey = KEY_SECURE;
+                }
             } else if (movedToSystem != null && movedToSystem.contains(key)) {
                 destination = Settings.System.CONTENT_URI;
+                if (areAgentMetricsEnabled) {
+                    finalSettingsKey = KEY_SYSTEM;
+                }
             } else {
                 destination = contentUri;
             }
@@ -977,6 +1027,10 @@
                 if (isSettingPreserved) {
                     Log.i(TAG, "Skipping restore for setting navigation_mode "
                         + "as it is marked as preserved");
+                    if (areAgentMetricsEnabled) {
+                        mBackupRestoreEventLogger.logItemsRestoreFailed(
+                                finalSettingsKey, /* count= */ 1, ERROR_SKIPPED_PRESERVED);
+                    }
                     continue;
                 }
             }
@@ -996,12 +1050,16 @@
                 Log.d(TAG, "Restored font scale from: " + toRestore + " to " + value);
             }
 
-
+            // TODO(b/379861078): Log metrics inside this method.
             settingsHelper.restoreValue(this, cr, contentValues, destination, key, value,
                     mRestoredFromSdkInt);
 
             Log.d(TAG, "Restored setting: " + destination + " : " + key + "=" + value);
+            if (areAgentMetricsEnabled) {
+                mBackupRestoreEventLogger.logItemsRestored(finalSettingsKey, /* count= */ 1);
+            }
         }
+
     }
 
 
@@ -1031,29 +1089,29 @@
     }
 
     @VisibleForTesting
-    SettingsBackupWhitelist getBackupWhitelist(Uri contentUri) {
+    SettingsBackupAllowlist getBackupAllowlist(Uri contentUri) {
         // Figure out the white list and redirects to the global table.  We restore anything
         // in either the backup allowlist or the legacy-restore allowlist for this table.
-        String[] whitelist;
+        String[] allowlist;
         Map<String, Validator> validators = null;
         if (contentUri.equals(Settings.Secure.CONTENT_URI)) {
-            whitelist = ArrayUtils.concat(String.class, SecureSettings.SETTINGS_TO_BACKUP,
+            allowlist = ArrayUtils.concat(String.class, SecureSettings.SETTINGS_TO_BACKUP,
                     Settings.Secure.LEGACY_RESTORE_SETTINGS,
                     DeviceSpecificSettings.DEVICE_SPECIFIC_SETTINGS_TO_BACKUP);
             validators = SecureSettingsValidators.VALIDATORS;
         } else if (contentUri.equals(Settings.System.CONTENT_URI)) {
-            whitelist = ArrayUtils.concat(String.class, SystemSettings.SETTINGS_TO_BACKUP,
+            allowlist = ArrayUtils.concat(String.class, SystemSettings.SETTINGS_TO_BACKUP,
                     Settings.System.LEGACY_RESTORE_SETTINGS);
             validators = SystemSettingsValidators.VALIDATORS;
         } else if (contentUri.equals(Settings.Global.CONTENT_URI)) {
-            whitelist = ArrayUtils.concat(String.class, getGlobalSettingsToBackup(),
+            allowlist = ArrayUtils.concat(String.class, getGlobalSettingsToBackup(),
                     Settings.Global.LEGACY_RESTORE_SETTINGS);
             validators = GlobalSettingsValidators.VALIDATORS;
         } else {
             throw new IllegalArgumentException("Unknown URI: " + contentUri);
         }
 
-        return new SettingsBackupWhitelist(whitelist, validators);
+        return new SettingsBackupAllowlist(allowlist, validators);
     }
 
     private String[] getGlobalSettingsToBackup() {
@@ -1449,7 +1507,8 @@
                 null,
                 blockedSettingsArrayId,
                 dynamicBlocklist,
-                preservedSettings);
+                preservedSettings,
+                KEY_DEVICE_SPECIFIC_CONFIG);
 
         updateWindowManagerIfNeeded(originalDensity);
 
@@ -1647,14 +1706,14 @@
      * Store the allowlist of settings to be backed up and validators for them.
      */
     @VisibleForTesting
-    static class SettingsBackupWhitelist {
-        final String[] mSettingsWhitelist;
+    static class SettingsBackupAllowlist {
+        final String[] mSettingsAllowlist;
         final Map<String, Validator> mSettingsValidators;
 
 
-        SettingsBackupWhitelist(String[] settingsWhitelist,
+        SettingsBackupAllowlist(String[] settingsAllowlist,
                 Map<String, Validator> settingsValidators) {
-            mSettingsWhitelist = settingsWhitelist;
+            mSettingsAllowlist = settingsAllowlist;
             mSettingsValidators = settingsValidators;
         }
     }
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java
index 4642864..350c149 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java
@@ -30,6 +30,7 @@
 
 import android.app.backup.BackupAnnotations.BackupDestination;
 import android.app.backup.BackupAnnotations.OperationType;
+import android.app.backup.BackupDataInput;
 import android.app.backup.BackupDataOutput;
 import android.app.backup.BackupRestoreEventLogger;
 import android.app.backup.BackupRestoreEventLogger.DataTypeResult;
@@ -57,6 +58,7 @@
 
 import com.android.window.flags.Flags;
 
+import java.util.List;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -95,6 +97,15 @@
     private static final Map<String, Validator> TEST_VALUES_VALIDATORS = new HashMap<>();
     private static final String TEST_KEY = "test_key";
     private static final String TEST_VALUE = "test_value";
+    private static final String ERROR_COULD_NOT_READ_ENTITY = "could_not_read_entity";
+    private static final String ERROR_SKIPPED_BY_SYSTEM = "skipped_by_system";
+    private static final String ERROR_SKIPPED_BY_BLOCKLIST =
+        "skipped_by_dynamic_blocklist";
+    private static final String ERROR_SKIPPED_PRESERVED = "skipped_preserved";
+    private static final String ERROR_DID_NOT_PASS_VALIDATION = "did_not_pass_validation";
+    private static final String KEY_SYSTEM = "system";
+    private static final String KEY_SECURE = "secure";
+    private static final String KEY_GLOBAL = "global";
 
     static {
         DEVICE_SPECIFIC_TEST_VALUES.put(Settings.Secure.DISPLAY_DENSITY_FORCED,
@@ -113,6 +124,7 @@
     @Rule
     public final MockitoRule mockito = MockitoJUnit.rule();
 
+    @Mock private BackupDataInput mBackupDataInput;
     @Mock private BackupDataOutput mBackupDataOutput;
 
     private TestFriendlySettingsBackupAgent mAgentUnderTest;
@@ -232,19 +244,32 @@
 
     @Test
     public void testOnRestore_preservedSettingsAreNotRestored() {
-        SettingsBackupAgent.SettingsBackupWhitelist whitelist =
-                new SettingsBackupAgent.SettingsBackupWhitelist(
+        SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+                new SettingsBackupAgent.SettingsBackupAllowlist(
                         new String[] { OVERRIDDEN_TEST_SETTING, PRESERVED_TEST_SETTING },
                         TEST_VALUES_VALIDATORS);
-        mAgentUnderTest.setSettingsWhitelist(whitelist);
+        mAgentUnderTest.setSettingsAllowlist(allowlist);
         mAgentUnderTest.setBlockedSettings();
         TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
         mAgentUnderTest.mSettingsHelper = settingsHelper;
 
         byte[] backupData = generateBackupData(TEST_VALUES);
-        mAgentUnderTest.restoreSettings(backupData, /* pos */ 0, backupData.length, TEST_URI,
-                null, null, null, /* blockedSettingsArrayId */ 0, Collections.emptySet(),
-                new HashSet<>(Collections.singletonList(SettingsBackupAgent.getQualifiedKeyForSetting(PRESERVED_TEST_SETTING, TEST_URI))));
+        mAgentUnderTest.restoreSettings(
+            backupData,
+            /* pos */ 0,
+            backupData.length,
+            TEST_URI,
+            null,
+            null,
+            null,
+            /* blockedSettingsArrayId */ 0,
+            Collections.emptySet(),
+            new HashSet<>(Collections
+                              .singletonList(
+                                  SettingsBackupAgent
+                                      .getQualifiedKeyForSetting(
+                                          PRESERVED_TEST_SETTING, TEST_URI))),
+            TEST_KEY);
 
         assertTrue(settingsHelper.mWrittenValues.containsKey(OVERRIDDEN_TEST_SETTING));
         assertFalse(settingsHelper.mWrittenValues.containsKey(PRESERVED_TEST_SETTING));
@@ -395,6 +420,382 @@
         assertNull(getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest));
     }
 
+    @Test
+    @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void restoreSettings_agentMetricsAreEnabled_agentMetricsAreLogged() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+        SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+                new SettingsBackupAgent.SettingsBackupAllowlist(
+                        new String[] {OVERRIDDEN_TEST_SETTING},
+                        TEST_VALUES_VALIDATORS);
+        mAgentUnderTest.setSettingsAllowlist(allowlist);
+        mAgentUnderTest.setBlockedSettings();
+        TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+        mAgentUnderTest.mSettingsHelper = settingsHelper;
+
+        byte[] backupData = generateBackupData(TEST_VALUES);
+        mAgentUnderTest
+            .restoreSettings(
+                backupData,
+                /* pos= */ 0,
+                backupData.length,
+                TEST_URI,
+                /* movedToGlobal= */ null,
+                /* movedToSecure= */ null,
+                /* movedToSystem= */ null,
+                /* blockedSettingsArrayId= */ 0,
+                /* dynamicBlockList= */ Collections.emptySet(),
+                /* settingsToPreserve= */ Collections.emptySet(),
+                TEST_KEY);
+
+        DataTypeResult loggingResult =
+            getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest);
+        assertNotNull(loggingResult);
+        assertEquals(loggingResult.getSuccessCount(), 1);
+    }
+
+    @Test
+    @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void restoreSettings_agentMetricsAreDisabled_agentMetricsAreNotLogged() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+        SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+                new SettingsBackupAgent.SettingsBackupAllowlist(
+                        new String[] {OVERRIDDEN_TEST_SETTING},
+                        TEST_VALUES_VALIDATORS);
+        mAgentUnderTest.setSettingsAllowlist(allowlist);
+        mAgentUnderTest.setBlockedSettings();
+        TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+        mAgentUnderTest.mSettingsHelper = settingsHelper;
+
+        byte[] backupData = generateBackupData(TEST_VALUES);
+        mAgentUnderTest
+            .restoreSettings(
+                backupData,
+                /* pos= */ 0,
+                backupData.length,
+                TEST_URI,
+                /* movedToGlobal= */ null,
+                /* movedToSecure= */ null,
+                /* movedToSystem= */ null,
+                /* blockedSettingsArrayId= */ 0,
+                /* dynamicBlockList= */ Collections.emptySet(),
+                /* settingsToPreserve= */ Collections.emptySet(),
+                TEST_KEY);
+
+        DataTypeResult loggingResult =
+            getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest);
+        assertNull(loggingResult);
+    }
+
+    @Test
+    @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void restoreSettings_agentMetricsAreEnabled_readEntityDataFails_failureIsLogged()
+        throws IOException {
+        when(mBackupDataInput.readEntityData(any(byte[].class), anyInt(), anyInt()))
+            .thenThrow(new IOException());
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+
+        mAgentUnderTest.restoreSettings(
+            mBackupDataInput,
+            TEST_URI,
+            /* movedToGlobal= */ null,
+            /* movedToSecure= */ null,
+            /* movedToSystem= */ null,
+            /* blockedSettingsArrayId= */ 0,
+            /* dynamicBlockList= */ Collections.emptySet(),
+            /* settingsToPreserve= */ Collections.emptySet(),
+            TEST_KEY);
+
+        DataTypeResult loggingResult =
+            getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest);
+        assertNotNull(loggingResult);
+        assertEquals(loggingResult.getFailCount(), 1);
+        assertTrue(loggingResult.getErrors().containsKey(ERROR_COULD_NOT_READ_ENTITY));
+    }
+
+    @Test
+    @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void restoreSettings_agentMetricsAreDisabled_readEntityDataFails_failureIsNotLogged()
+        throws IOException {
+        when(mBackupDataInput.readEntityData(any(byte[].class), anyInt(), anyInt()))
+            .thenThrow(new IOException());
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+
+        mAgentUnderTest.restoreSettings(
+            mBackupDataInput,
+            TEST_URI,
+            /* movedToGlobal= */ null,
+            /* movedToSecure= */ null,
+            /* movedToSystem= */ null,
+            /* blockedSettingsArrayId= */ 0,
+            /* dynamicBlockList= */ Collections.emptySet(),
+            /* settingsToPreserve= */ Collections.emptySet(),
+            TEST_KEY);
+
+        assertNull(getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest));
+    }
+
+    @Test
+    @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void restoreSettings_agentMetricsAreEnabled_settingIsSkippedBySystem_failureIsLogged() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+        String[] settingBlockedBySystem = new String[] {OVERRIDDEN_TEST_SETTING};
+        SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+                new SettingsBackupAgent.SettingsBackupAllowlist(
+                        settingBlockedBySystem,
+                        TEST_VALUES_VALIDATORS);
+        mAgentUnderTest.setSettingsAllowlist(allowlist);
+        mAgentUnderTest.setBlockedSettings(settingBlockedBySystem);
+        TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+        mAgentUnderTest.mSettingsHelper = settingsHelper;
+
+        byte[] backupData = generateBackupData(TEST_VALUES);
+        mAgentUnderTest
+            .restoreSettings(
+                backupData,
+                /* pos= */ 0,
+                backupData.length,
+                TEST_URI,
+                /* movedToGlobal= */ null,
+                /* movedToSecure= */ null,
+                /* movedToSystem= */ null,
+                /* blockedSettingsArrayId= */ 0,
+                /* dynamicBlockList= */ Collections.emptySet(),
+                /* settingsToPreserve= */ Collections.emptySet(),
+                TEST_KEY);
+
+        DataTypeResult loggingResult =
+            getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest);
+        assertNotNull(loggingResult);
+        assertEquals(loggingResult.getFailCount(), 1);
+        assertTrue(loggingResult.getErrors().containsKey(ERROR_SKIPPED_BY_SYSTEM));
+    }
+
+    @Test
+    @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void restoreSettings_agentMetricsAreEnabled_settingIsSkippedByBlockList_failureIsLogged() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+        SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+                new SettingsBackupAgent.SettingsBackupAllowlist(
+                        new String[] {OVERRIDDEN_TEST_SETTING},
+                        TEST_VALUES_VALIDATORS);
+        mAgentUnderTest.setSettingsAllowlist(allowlist);
+        mAgentUnderTest.setBlockedSettings();
+        TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+        mAgentUnderTest.mSettingsHelper = settingsHelper;
+        Set<String> dynamicBlockList =
+            Set.of(Uri.withAppendedPath(TEST_URI, OVERRIDDEN_TEST_SETTING).toString());
+
+        byte[] backupData = generateBackupData(TEST_VALUES);
+        mAgentUnderTest
+            .restoreSettings(
+                backupData,
+                /* pos= */ 0,
+                backupData.length,
+                TEST_URI,
+                /* movedToGlobal= */ null,
+                /* movedToSecure= */ null,
+                /* movedToSystem= */ null,
+                /* blockedSettingsArrayId= */ 0,
+                dynamicBlockList,
+                /* settingsToPreserve= */ Collections.emptySet(),
+                TEST_KEY);
+
+        DataTypeResult loggingResult =
+            getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest);
+        assertNotNull(loggingResult);
+        assertEquals(loggingResult.getFailCount(), 1);
+        assertTrue(loggingResult.getErrors().containsKey(ERROR_SKIPPED_BY_BLOCKLIST));
+    }
+
+    @Test
+    @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void restoreSettings_agentMetricsAreEnabled_settingIsPreserved_failureIsLogged() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+        SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+                new SettingsBackupAgent.SettingsBackupAllowlist(
+                        new String[] {OVERRIDDEN_TEST_SETTING},
+                        TEST_VALUES_VALIDATORS);
+        mAgentUnderTest.setSettingsAllowlist(allowlist);
+        mAgentUnderTest.setBlockedSettings();
+        TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+        mAgentUnderTest.mSettingsHelper = settingsHelper;
+        Set<String> preservedSettings =
+            Set.of(Uri.withAppendedPath(TEST_URI, OVERRIDDEN_TEST_SETTING).toString());
+
+        byte[] backupData = generateBackupData(TEST_VALUES);
+        mAgentUnderTest
+            .restoreSettings(
+                backupData,
+                /* pos= */ 0,
+                backupData.length,
+                TEST_URI,
+                /* movedToGlobal= */ null,
+                /* movedToSecure= */ null,
+                /* movedToSystem= */ null,
+                /* blockedSettingsArrayId= */ 0,
+                /* dynamicBlockList = */ Collections.emptySet(),
+                preservedSettings,
+                TEST_KEY);
+
+        DataTypeResult loggingResult =
+            getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest);
+        assertNotNull(loggingResult);
+        assertEquals(loggingResult.getFailCount(), 1);
+        assertTrue(loggingResult.getErrors().containsKey(ERROR_SKIPPED_PRESERVED));
+    }
+
+    @Test
+    @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void restoreSettings_agentMetricsAreEnabled_settingIsNotValid_failureIsLogged() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+        SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+                new SettingsBackupAgent.SettingsBackupAllowlist(
+                        new String[] {OVERRIDDEN_TEST_SETTING},
+                        /* settingsValidators= */ null);
+        mAgentUnderTest.setSettingsAllowlist(allowlist);
+        mAgentUnderTest.setBlockedSettings();
+        TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+        mAgentUnderTest.mSettingsHelper = settingsHelper;
+
+        byte[] backupData = generateBackupData(TEST_VALUES);
+        mAgentUnderTest
+            .restoreSettings(
+                backupData,
+                /* pos= */ 0,
+                backupData.length,
+                TEST_URI,
+                /* movedToGlobal= */ null,
+                /* movedToSecure= */ null,
+                /* movedToSystem= */ null,
+                /* blockedSettingsArrayId= */ 0,
+                /* dynamicBlockList = */ Collections.emptySet(),
+                /* settingsToPreserve= */ Collections.emptySet(),
+                TEST_KEY);
+
+        DataTypeResult loggingResult =
+            getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest);
+        assertNotNull(loggingResult);
+        assertEquals(loggingResult.getFailCount(), 1);
+        assertTrue(loggingResult.getErrors().containsKey(ERROR_DID_NOT_PASS_VALIDATION));
+    }
+
+    @Test
+    @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void restoreSettings_agentMetricsAreEnabled_settingIsMarkedAsMovedToGlobal_agentMetricsAreLoggedWithGlobalKey() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+        SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+                new SettingsBackupAgent.SettingsBackupAllowlist(
+                        new String[] {OVERRIDDEN_TEST_SETTING},
+                        TEST_VALUES_VALIDATORS);
+        mAgentUnderTest.setSettingsAllowlist(allowlist);
+        mAgentUnderTest.setBlockedSettings();
+        TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+        mAgentUnderTest.mSettingsHelper = settingsHelper;
+
+        byte[] backupData = generateBackupData(TEST_VALUES);
+        mAgentUnderTest
+            .restoreSettings(
+                backupData,
+                /* pos= */ 0,
+                backupData.length,
+                TEST_URI,
+                /* movedToGlobal= */ Set.of(OVERRIDDEN_TEST_SETTING),
+                /* movedToSecure= */ null,
+                /* movedToSystem= */ null,
+                /* blockedSettingsArrayId= */ 0,
+                /* dynamicBlockList= */ Collections.emptySet(),
+                /* settingsToPreserve= */ Collections.emptySet(),
+                TEST_KEY);
+
+        DataTypeResult loggingResult =
+            getLoggingResultForDatatype(KEY_GLOBAL, mAgentUnderTest);
+        assertNotNull(loggingResult);
+        assertEquals(loggingResult.getSuccessCount(), 1);
+        assertNull(getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest));
+    }
+
+    @Test
+    @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void restoreSettings_agentMetricsAreEnabled_settingIsMarkedAsMovedToSecure_agentMetricsAreLoggedWithSecureKey() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+        SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+                new SettingsBackupAgent.SettingsBackupAllowlist(
+                        new String[] {OVERRIDDEN_TEST_SETTING},
+                        TEST_VALUES_VALIDATORS);
+        mAgentUnderTest.setSettingsAllowlist(allowlist);
+        mAgentUnderTest.setBlockedSettings();
+        TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+        mAgentUnderTest.mSettingsHelper = settingsHelper;
+
+        byte[] backupData = generateBackupData(TEST_VALUES);
+        mAgentUnderTest
+            .restoreSettings(
+                backupData,
+                /* pos= */ 0,
+                backupData.length,
+                TEST_URI,
+                /* movedToGlobal= */ null,
+                /* movedToSecure= */ Set.of(OVERRIDDEN_TEST_SETTING),
+                /* movedToSystem= */ null,
+                /* blockedSettingsArrayId= */ 0,
+                /* dynamicBlockList= */ Collections.emptySet(),
+                /* settingsToPreserve= */ Collections.emptySet(),
+                TEST_KEY);
+
+        DataTypeResult loggingResult =
+            getLoggingResultForDatatype(KEY_SECURE, mAgentUnderTest);
+        assertNotNull(loggingResult);
+        assertEquals(loggingResult.getSuccessCount(), 1);
+        assertNull(getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest));
+    }
+
+    @Test
+    @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void restoreSettings_agentMetricsAreEnabled_settingIsMarkedAsMovedToSystem_agentMetricsAreLoggedWithSystemKey() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+        SettingsBackupAgent.SettingsBackupAllowlist allowlist =
+                new SettingsBackupAgent.SettingsBackupAllowlist(
+                        new String[] {OVERRIDDEN_TEST_SETTING},
+                        TEST_VALUES_VALIDATORS);
+        mAgentUnderTest.setSettingsAllowlist(allowlist);
+        mAgentUnderTest.setBlockedSettings();
+        TestSettingsHelper settingsHelper = new TestSettingsHelper(mContext);
+        mAgentUnderTest.mSettingsHelper = settingsHelper;
+
+        byte[] backupData = generateBackupData(TEST_VALUES);
+        mAgentUnderTest
+            .restoreSettings(
+                backupData,
+                /* pos= */ 0,
+                backupData.length,
+                TEST_URI,
+                /* movedToGlobal= */ null,
+                /* movedToSecure= */ null,
+                /* movedToSystem= */ Set.of(OVERRIDDEN_TEST_SETTING),
+                /* blockedSettingsArrayId= */ 0,
+                /* dynamicBlockList= */ Collections.emptySet(),
+                /* settingsToPreserve= */ Collections.emptySet(),
+                TEST_KEY);
+
+        DataTypeResult loggingResult =
+            getLoggingResultForDatatype(KEY_SYSTEM, mAgentUnderTest);
+        assertNotNull(loggingResult);
+        assertEquals(loggingResult.getSuccessCount(), 1);
+        assertNull(getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest));
+    }
+
     private byte[] generateBackupData(Map<String, String> keyValueData) {
         int totalBytes = 0;
         for (String key : keyValueData.keySet()) {
@@ -426,7 +827,8 @@
                 null,
                 R.array.restore_blocked_global_settings,
                 /* dynamicBlockList= */ Collections.emptySet(),
-                /* settingsToPreserve= */ Collections.emptySet());
+                /* settingsToPreserve= */ Collections.emptySet(),
+                TEST_KEY);
     }
 
     private byte[] generateUncorruptedHeader() throws IOException {
@@ -488,7 +890,7 @@
     private static class TestFriendlySettingsBackupAgent extends SettingsBackupAgent {
         private Boolean mForcedDeviceInfoRestoreAcceptability = null;
         private String[] mBlockedSettings = null;
-        private SettingsBackupWhitelist mSettingsWhitelist = null;
+        private SettingsBackupAllowlist mSettingsAllowlist = null;
 
         void setForcedDeviceInfoRestoreAcceptability(boolean value) {
             mForcedDeviceInfoRestoreAcceptability = value;
@@ -498,8 +900,8 @@
             mBlockedSettings = blockedSettings;
         }
 
-        void setSettingsWhitelist(SettingsBackupWhitelist settingsWhitelist) {
-            mSettingsWhitelist = settingsWhitelist;
+        void setSettingsAllowlist(SettingsBackupAllowlist settingsAllowlist) {
+            mSettingsAllowlist = settingsAllowlist;
         }
 
         @Override
@@ -517,12 +919,12 @@
         }
 
         @Override
-        SettingsBackupWhitelist getBackupWhitelist(Uri contentUri) {
-            if (mSettingsWhitelist == null) {
-                return super.getBackupWhitelist(contentUri);
+        SettingsBackupAllowlist getBackupAllowlist(Uri contentUri) {
+            if (mSettingsAllowlist == null) {
+                return super.getBackupAllowlist(contentUri);
             }
 
-            return mSettingsWhitelist;
+            return mSettingsAllowlist;
         }
 
         void setNumberOfSettingsPerKey(String key, int numberOfSettings) {
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
index 58b8836..9fe85b7 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
@@ -111,22 +111,24 @@
     draggable: NestedDraggable,
     orientation: Orientation,
     overscrollEffect: OverscrollEffect? = null,
+    enabled: Boolean = true,
 ): Modifier {
     return this.thenIf(overscrollEffect != null) { Modifier.overscroll(overscrollEffect) }
-        .then(NestedDraggableElement(draggable, orientation, overscrollEffect))
+        .then(NestedDraggableElement(draggable, orientation, overscrollEffect, enabled))
 }
 
 private data class NestedDraggableElement(
     private val draggable: NestedDraggable,
     private val orientation: Orientation,
     private val overscrollEffect: OverscrollEffect?,
+    private val enabled: Boolean,
 ) : ModifierNodeElement<NestedDraggableNode>() {
     override fun create(): NestedDraggableNode {
-        return NestedDraggableNode(draggable, orientation, overscrollEffect)
+        return NestedDraggableNode(draggable, orientation, overscrollEffect, enabled)
     }
 
     override fun update(node: NestedDraggableNode) {
-        node.update(draggable, orientation, overscrollEffect)
+        node.update(draggable, orientation, overscrollEffect, enabled)
     }
 }
 
@@ -134,6 +136,7 @@
     private var draggable: NestedDraggable,
     override var orientation: Orientation,
     private var overscrollEffect: OverscrollEffect?,
+    private var enabled: Boolean,
 ) :
     DelegatingNode(),
     PointerInputModifierNode,
@@ -179,14 +182,22 @@
         draggable: NestedDraggable,
         orientation: Orientation,
         overscrollEffect: OverscrollEffect?,
+        enabled: Boolean,
     ) {
         this.draggable = draggable
         this.orientation = orientation
         this.overscrollEffect = overscrollEffect
+        this.enabled = enabled
 
         trackDownPositionDelegate?.resetPointerInputHandler()
         detectDragsDelegate?.resetPointerInputHandler()
         nestedScrollController?.ensureOnDragStoppedIsCalled()
+
+        if (!enabled && trackDownPositionDelegate != null) {
+            check(detectDragsDelegate != null)
+            trackDownPositionDelegate = null
+            detectDragsDelegate = null
+        }
     }
 
     override fun onPointerEvent(
@@ -194,6 +205,8 @@
         pass: PointerEventPass,
         bounds: IntSize,
     ) {
+        if (!enabled) return
+
         if (trackDownPositionDelegate == null) {
             check(detectDragsDelegate == null)
             trackDownPositionDelegate = SuspendingPointerInputModifierNode { trackDownPosition() }
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
index f8561b8..fd3902f 100644
--- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
@@ -344,6 +344,45 @@
         assertThat(draggable.onDragStoppedCalled).isTrue()
     }
 
+    @Test
+    fun enabled() {
+        val draggable = TestDraggable()
+        var enabled by mutableStateOf(false)
+        val touchSlop =
+            rule.setContentWithTouchSlop {
+                Box(
+                    Modifier.fillMaxSize()
+                        .nestedDraggable(draggable, orientation, enabled = enabled)
+                )
+            }
+
+        assertThat(draggable.onDragStartedCalled).isFalse()
+
+        rule.onRoot().performTouchInput {
+            down(center)
+            moveBy(touchSlop.toOffset())
+        }
+
+        assertThat(draggable.onDragStartedCalled).isFalse()
+        assertThat(draggable.onDragStoppedCalled).isFalse()
+
+        enabled = true
+        rule.onRoot().performTouchInput {
+            // Release previously up finger.
+            up()
+
+            down(center)
+            moveBy(touchSlop.toOffset())
+        }
+
+        assertThat(draggable.onDragStartedCalled).isTrue()
+        assertThat(draggable.onDragStoppedCalled).isFalse()
+
+        enabled = false
+        rule.waitForIdle()
+        assertThat(draggable.onDragStoppedCalled).isTrue()
+    }
+
     private fun ComposeContentTestRule.setContentWithTouchSlop(
         content: @Composable () -> Unit
     ): Float {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
index 52c476e..e4a9888 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.nullable
 import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -136,4 +137,13 @@
 
             verify(wifiStateWorker, times(1)).isWifiEnabled = eq(true)
         }
+
+    @Test
+    fun detailsViewModel() =
+        kosmos.testScope.runTest {
+            assertThat(underTest.detailsViewModel.getTitle())
+                .isEqualTo("Internet")
+            assertThat(underTest.detailsViewModel.getSubTitle())
+                .isEqualTo("Tab a network to connect")
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
index 954215ee..2edb9c6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
@@ -173,6 +173,21 @@
                 .isEqualTo(FakeQSTileDataInteractor.AvailabilityRequest(USER))
         }
 
+    @Test
+    fun tileDetails() =
+        testScope.runTest {
+            assertThat(tileUserActionInteractor.detailsViewModel).isNotNull()
+            assertThat(tileUserActionInteractor.detailsViewModel?.getTitle())
+                .isEqualTo("FakeQSTileUserActionInteractor")
+            assertThat(underTest.detailsViewModel).isNotNull()
+            assertThat(underTest.detailsViewModel?.getTitle())
+                .isEqualTo("FakeQSTileUserActionInteractor")
+
+            tileUserActionInteractor.detailsViewModel = null
+            assertThat(tileUserActionInteractor.detailsViewModel).isNull()
+            assertThat(underTest.detailsViewModel).isNull()
+        }
+
     private fun createViewModel(
         scope: TestScope,
         config: QSTileConfig = tileConfig,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplOldTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplOldTest.kt
deleted file mode 100644
index 8b4f53a..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplOldTest.kt
+++ /dev/null
@@ -1,698 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.statusbar.notification.headsup
-
-import android.app.Notification
-import android.app.PendingIntent
-import android.app.Person
-import android.os.Handler
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
-import android.platform.test.flag.junit.FlagsParameterization
-import android.testing.TestableLooper.RunWithLooper
-import androidx.test.filters.SmallTest
-import com.android.internal.logging.testing.UiEventLoggerFake
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.kosmos.KosmosJavaAdapter
-import com.android.systemui.log.logcatLogBuffer
-import com.android.systemui.res.R
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
-import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl
-import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerImpl.HeadsUpEntry
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
-import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
-import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.concurrency.mockExecutorHandler
-import com.android.systemui.util.kotlin.JavaAdapter
-import com.android.systemui.util.settings.FakeGlobalSettings
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.MutableStateFlow
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.invocation.InvocationOnMock
-import org.mockito.junit.MockitoJUnit
-import org.mockito.junit.MockitoRule
-import org.mockito.kotlin.eq
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4
-import platform.test.runner.parameterized.Parameters
-
-@SmallTest
-@RunWithLooper
-@RunWith(ParameterizedAndroidJunit4::class)
-// TODO(b/378142453): Merge this with HeadsUpManagerImplTest.
-open class HeadsUpManagerImplOldTest(flags: FlagsParameterization?) : SysuiTestCase() {
-    protected var mKosmos: KosmosJavaAdapter = KosmosJavaAdapter(this)
-
-    @JvmField @Rule var rule: MockitoRule = MockitoJUnit.rule()
-
-    private val mUiEventLoggerFake = UiEventLoggerFake()
-
-    private val mLogger: HeadsUpManagerLogger = Mockito.spy(HeadsUpManagerLogger(logcatLogBuffer()))
-
-    @Mock private val mBgHandler: Handler? = null
-
-    @Mock private val dumpManager: DumpManager? = null
-
-    @Mock private val mShadeInteractor: ShadeInteractor? = null
-    private var mAvalancheController: AvalancheController? = null
-
-    @Mock private val mAccessibilityMgr: AccessibilityManagerWrapper? = null
-
-    protected val globalSettings: FakeGlobalSettings = FakeGlobalSettings()
-    protected val systemClock: FakeSystemClock = FakeSystemClock()
-    protected val executor: FakeExecutor = FakeExecutor(systemClock)
-
-    @Mock protected var mRow: ExpandableNotificationRow? = null
-
-    private fun createHeadsUpManager(): HeadsUpManagerImpl {
-        return HeadsUpManagerImpl(
-            mContext,
-            mLogger,
-            mKosmos.statusBarStateController,
-            mKosmos.keyguardBypassController,
-            GroupMembershipManagerImpl(),
-            mKosmos.visualStabilityProvider,
-            mKosmos.configurationController,
-            mockExecutorHandler(executor),
-            globalSettings,
-            systemClock,
-            executor,
-            mAccessibilityMgr,
-            mUiEventLoggerFake,
-            JavaAdapter(mKosmos.testScope),
-            mShadeInteractor,
-            mAvalancheController,
-        )
-    }
-
-    private fun createStickyEntry(id: Int): NotificationEntry {
-        val notif =
-            Notification.Builder(mContext, "")
-                .setSmallIcon(R.drawable.ic_person)
-                .setFullScreenIntent(
-                    Mockito.mock(PendingIntent::class.java), /* highPriority */
-                    true,
-                )
-                .build()
-        return HeadsUpManagerTestUtil.createEntry(id, notif)
-    }
-
-    private fun createStickyForSomeTimeEntry(id: Int): NotificationEntry {
-        val notif =
-            Notification.Builder(mContext, "")
-                .setSmallIcon(R.drawable.ic_person)
-                .setFlag(Notification.FLAG_FSI_REQUESTED_BUT_DENIED, true)
-                .build()
-        return HeadsUpManagerTestUtil.createEntry(id, notif)
-    }
-
-    private fun useAccessibilityTimeout(use: Boolean) {
-        if (use) {
-            Mockito.doReturn(TEST_A11Y_AUTO_DISMISS_TIME)
-                .`when`(mAccessibilityMgr!!)
-                .getRecommendedTimeoutMillis(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())
-        } else {
-            Mockito.`when`(
-                    mAccessibilityMgr!!.getRecommendedTimeoutMillis(
-                        ArgumentMatchers.anyInt(),
-                        ArgumentMatchers.anyInt(),
-                    )
-                )
-                .then { i: InvocationOnMock -> i.getArgument(0) }
-        }
-    }
-
-    init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
-    }
-
-    @Throws(Exception::class)
-    override fun SysuiSetup() {
-        super.SysuiSetup()
-        mContext.getOrCreateTestableResources().apply {
-            this.addOverride(R.integer.ambient_notification_extension_time, TEST_EXTENSION_TIME)
-            this.addOverride(R.integer.touch_acceptance_delay, TEST_TOUCH_ACCEPTANCE_TIME)
-            this.addOverride(
-                R.integer.heads_up_notification_minimum_time,
-                TEST_MINIMUM_DISPLAY_TIME,
-            )
-            this.addOverride(
-                R.integer.heads_up_notification_minimum_time_with_throttling,
-                TEST_MINIMUM_DISPLAY_TIME,
-            )
-            this.addOverride(R.integer.heads_up_notification_decay, TEST_AUTO_DISMISS_TIME)
-            this.addOverride(
-                R.integer.sticky_heads_up_notification_time,
-                TEST_STICKY_AUTO_DISMISS_TIME,
-            )
-        }
-
-        mAvalancheController =
-            AvalancheController(dumpManager!!, mUiEventLoggerFake, mLogger, mBgHandler!!)
-        Mockito.`when`(mShadeInteractor!!.isAnyExpanded).thenReturn(MutableStateFlow(true))
-        Mockito.`when`(mKosmos.keyguardBypassController.bypassEnabled).thenReturn(false)
-    }
-
-    @Test
-    fun testHasNotifications_headsUpManagerMapNotEmpty_true() {
-        val bhum = createHeadsUpManager()
-        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-        bhum.showNotification(entry)
-
-        Truth.assertThat(bhum.mHeadsUpEntryMap).isNotEmpty()
-        Truth.assertThat(bhum.hasNotifications()).isTrue()
-    }
-
-    @Test
-    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    fun testHasNotifications_avalancheMapNotEmpty_true() {
-        val bhum = createHeadsUpManager()
-        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-        val headsUpEntry = bhum.createHeadsUpEntry(notifEntry)
-        mAvalancheController!!.addToNext(headsUpEntry) {}
-
-        Truth.assertThat(mAvalancheController!!.getWaitingEntryList()).isNotEmpty()
-        Truth.assertThat(bhum.hasNotifications()).isTrue()
-    }
-
-    @Test
-    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    fun testHasNotifications_false() {
-        val bhum = createHeadsUpManager()
-        Truth.assertThat(bhum.mHeadsUpEntryMap).isEmpty()
-        Truth.assertThat(mAvalancheController!!.getWaitingEntryList()).isEmpty()
-        Truth.assertThat(bhum.hasNotifications()).isFalse()
-    }
-
-    @Test
-    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    fun testGetHeadsUpEntryList_includesAvalancheEntryList() {
-        val bhum = createHeadsUpManager()
-        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-        val headsUpEntry = bhum.createHeadsUpEntry(notifEntry)
-        mAvalancheController!!.addToNext(headsUpEntry) {}
-
-        Truth.assertThat(bhum.headsUpEntryList).contains(headsUpEntry)
-    }
-
-    @Test
-    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    fun testGetHeadsUpEntry_returnsAvalancheEntry() {
-        val bhum = createHeadsUpManager()
-        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-        val headsUpEntry = bhum.createHeadsUpEntry(notifEntry)
-        mAvalancheController!!.addToNext(headsUpEntry) {}
-
-        Truth.assertThat(bhum.getHeadsUpEntry(notifEntry.key)).isEqualTo(headsUpEntry)
-    }
-
-    @Test
-    fun testShowNotification_addsEntry() {
-        val alm = createHeadsUpManager()
-        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
-        alm.showNotification(entry)
-
-        assertThat(alm.isHeadsUpEntry(entry.key)).isTrue()
-        assertThat(alm.hasNotifications()).isTrue()
-        assertThat(alm.getEntry(entry.key)).isEqualTo(entry)
-    }
-
-    @Test
-    fun testShowNotification_autoDismisses() {
-        val alm = createHeadsUpManager()
-        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
-        alm.showNotification(entry)
-        systemClock.advanceTime((TEST_AUTO_DISMISS_TIME * 3 / 2).toLong())
-
-        assertThat(alm.isHeadsUpEntry(entry.key)).isFalse()
-    }
-
-    @Test
-    fun testRemoveNotification_removeDeferred() {
-        val alm = createHeadsUpManager()
-        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
-        alm.showNotification(entry)
-
-        val removedImmediately =
-            alm.removeNotification(entry.key, /* releaseImmediately= */ false, "removeDeferred")
-        assertThat(removedImmediately).isFalse()
-        assertThat(alm.isHeadsUpEntry(entry.key)).isTrue()
-    }
-
-    @Test
-    fun testRemoveNotification_forceRemove() {
-        val alm = createHeadsUpManager()
-        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
-        alm.showNotification(entry)
-
-        val removedImmediately =
-            alm.removeNotification(entry.key, /* releaseImmediately= */ true, "forceRemove")
-        assertThat(removedImmediately).isTrue()
-        assertThat(alm.isHeadsUpEntry(entry.key)).isFalse()
-    }
-
-    @Test
-    fun testReleaseAllImmediately() {
-        val alm = createHeadsUpManager()
-        for (i in 0 until TEST_NUM_NOTIFICATIONS) {
-            val entry = HeadsUpManagerTestUtil.createEntry(i, mContext)
-            entry.row = mRow
-            alm.showNotification(entry)
-        }
-
-        alm.releaseAllImmediately()
-
-        assertThat(alm.allEntries.count()).isEqualTo(0)
-    }
-
-    @Test
-    fun testCanRemoveImmediately_notShownLongEnough() {
-        val alm = createHeadsUpManager()
-        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
-        alm.showNotification(entry)
-
-        // The entry has just been added so we should not remove immediately.
-        assertThat(alm.canRemoveImmediately(entry.key)).isFalse()
-    }
-
-    @Test
-    fun testHunRemovedLogging() {
-        val hum = createHeadsUpManager()
-        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-        val headsUpEntry = Mockito.mock(HeadsUpEntry::class.java)
-        Mockito.`when`(headsUpEntry.pinnedStatus)
-            .thenReturn(MutableStateFlow(PinnedStatus.NotPinned))
-        headsUpEntry.mEntry = notifEntry
-
-        hum.onEntryRemoved(headsUpEntry, "test")
-
-        Mockito.verify(mLogger, Mockito.times(1)).logNotificationActuallyRemoved(eq(notifEntry))
-    }
-
-    @Test
-    fun testShowNotification_autoDismissesIncludingTouchAcceptanceDelay() {
-        val hum = createHeadsUpManager()
-        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-        useAccessibilityTimeout(false)
-
-        hum.showNotification(entry)
-        systemClock.advanceTime((TEST_TOUCH_ACCEPTANCE_TIME / 2 + TEST_AUTO_DISMISS_TIME).toLong())
-
-        assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
-    }
-
-    @Test
-    fun testShowNotification_autoDismissesWithDefaultTimeout() {
-        val hum = createHeadsUpManager()
-        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-        useAccessibilityTimeout(false)
-
-        hum.showNotification(entry)
-        systemClock.advanceTime(
-            (TEST_TOUCH_ACCEPTANCE_TIME +
-                    (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
-                .toLong()
-        )
-
-        assertThat(hum.isHeadsUpEntry(entry.key)).isFalse()
-    }
-
-    @Test
-    fun testShowNotification_stickyForSomeTime_autoDismissesWithStickyTimeout() {
-        val hum = createHeadsUpManager()
-        val entry = createStickyForSomeTimeEntry(/* id= */ 0)
-        useAccessibilityTimeout(false)
-
-        hum.showNotification(entry)
-        systemClock.advanceTime(
-            (TEST_TOUCH_ACCEPTANCE_TIME +
-                    (TEST_AUTO_DISMISS_TIME + TEST_STICKY_AUTO_DISMISS_TIME) / 2)
-                .toLong()
-        )
-
-        assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
-    }
-
-    @Test
-    fun testShowNotification_sticky_neverAutoDismisses() {
-        val hum = createHeadsUpManager()
-        val entry = createStickyEntry(/* id= */ 0)
-        useAccessibilityTimeout(false)
-
-        hum.showNotification(entry)
-        systemClock.advanceTime(
-            (TEST_TOUCH_ACCEPTANCE_TIME + 2 * TEST_A11Y_AUTO_DISMISS_TIME).toLong()
-        )
-
-        assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
-    }
-
-    @Test
-    fun testShowNotification_autoDismissesWithAccessibilityTimeout() {
-        val hum = createHeadsUpManager()
-        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-        useAccessibilityTimeout(true)
-
-        hum.showNotification(entry)
-        systemClock.advanceTime(
-            (TEST_TOUCH_ACCEPTANCE_TIME +
-                    (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
-                .toLong()
-        )
-
-        assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
-    }
-
-    @Test
-    fun testShowNotification_stickyForSomeTime_autoDismissesWithAccessibilityTimeout() {
-        val hum = createHeadsUpManager()
-        val entry = createStickyForSomeTimeEntry(/* id= */ 0)
-        useAccessibilityTimeout(true)
-
-        hum.showNotification(entry)
-        systemClock.advanceTime(
-            (TEST_TOUCH_ACCEPTANCE_TIME +
-                    (TEST_STICKY_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
-                .toLong()
-        )
-
-        assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
-    }
-
-    @Test
-    fun testRemoveNotification_beforeMinimumDisplayTime() {
-        val hum = createHeadsUpManager()
-        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-        useAccessibilityTimeout(false)
-
-        hum.showNotification(entry)
-
-        val removedImmediately =
-            hum.removeNotification(
-                entry.key,
-                /* releaseImmediately = */ false,
-                "beforeMinimumDisplayTime",
-            )
-        assertThat(removedImmediately).isFalse()
-        assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
-
-        systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong())
-
-        assertThat(hum.isHeadsUpEntry(entry.key)).isFalse()
-    }
-
-    @Test
-    fun testRemoveNotification_afterMinimumDisplayTime() {
-        val hum = createHeadsUpManager()
-        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-        useAccessibilityTimeout(false)
-
-        hum.showNotification(entry)
-        systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong())
-
-        assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
-
-        val removedImmediately =
-            hum.removeNotification(
-                entry.key,
-                /* releaseImmediately = */ false,
-                "afterMinimumDisplayTime",
-            )
-        assertThat(removedImmediately).isTrue()
-        assertThat(hum.isHeadsUpEntry(entry.key)).isFalse()
-    }
-
-    @Test
-    fun testRemoveNotification_releaseImmediately() {
-        val hum = createHeadsUpManager()
-        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
-        hum.showNotification(entry)
-
-        val removedImmediately =
-            hum.removeNotification(
-                entry.key,
-                /* releaseImmediately = */ true,
-                "afterMinimumDisplayTime",
-            )
-        assertThat(removedImmediately).isTrue()
-        assertThat(hum.isHeadsUpEntry(entry.key)).isFalse()
-    }
-
-    @Test
-    fun testIsSticky_rowPinnedAndExpanded_true() {
-        val hum = createHeadsUpManager()
-        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-        Mockito.`when`(mRow!!.isPinned).thenReturn(true)
-        notifEntry.row = mRow
-
-        hum.showNotification(notifEntry)
-
-        val headsUpEntry = hum.getHeadsUpEntry(notifEntry.key)
-        headsUpEntry!!.setExpanded(true)
-
-        assertThat(hum.isSticky(notifEntry.key)).isTrue()
-    }
-
-    @Test
-    fun testIsSticky_remoteInputActive_true() {
-        val hum = createHeadsUpManager()
-        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
-        hum.showNotification(notifEntry)
-
-        val headsUpEntry = hum.getHeadsUpEntry(notifEntry.key)
-        headsUpEntry!!.mRemoteInputActive = true
-
-        assertThat(hum.isSticky(notifEntry.key)).isTrue()
-    }
-
-    @Test
-    fun testIsSticky_hasFullScreenIntent_true() {
-        val hum = createHeadsUpManager()
-        val notifEntry = HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
-
-        hum.showNotification(notifEntry)
-
-        assertThat(hum.isSticky(notifEntry.key)).isTrue()
-    }
-
-    @Test
-    fun testIsSticky_stickyForSomeTime_false() {
-        val hum = createHeadsUpManager()
-        val entry = createStickyForSomeTimeEntry(/* id= */ 0)
-
-        hum.showNotification(entry)
-
-        assertThat(hum.isSticky(entry.key)).isFalse()
-    }
-
-    @Test
-    fun testIsSticky_false() {
-        val hum = createHeadsUpManager()
-        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
-        hum.showNotification(notifEntry)
-
-        val headsUpEntry = hum.getHeadsUpEntry(notifEntry.key)
-        headsUpEntry!!.setExpanded(false)
-        headsUpEntry.mRemoteInputActive = false
-
-        assertThat(hum.isSticky(notifEntry.key)).isFalse()
-    }
-
-    @Test
-    fun testCompareTo_withNullEntries() {
-        val hum = createHeadsUpManager()
-        val alertEntry = NotificationEntryBuilder().setTag("alert").build()
-
-        hum.showNotification(alertEntry)
-
-        assertThat(hum.compare(alertEntry, null)).isLessThan(0)
-        assertThat(hum.compare(null, alertEntry)).isGreaterThan(0)
-        assertThat(hum.compare(null, null)).isEqualTo(0)
-    }
-
-    @Test
-    fun testCompareTo_withNonAlertEntries() {
-        val hum = createHeadsUpManager()
-
-        val nonAlertEntry1 = NotificationEntryBuilder().setTag("nae1").build()
-        val nonAlertEntry2 = NotificationEntryBuilder().setTag("nae2").build()
-        val alertEntry = NotificationEntryBuilder().setTag("alert").build()
-        hum.showNotification(alertEntry)
-
-        assertThat(hum.compare(alertEntry, nonAlertEntry1)).isLessThan(0)
-        assertThat(hum.compare(nonAlertEntry1, alertEntry)).isGreaterThan(0)
-        assertThat(hum.compare(nonAlertEntry1, nonAlertEntry2)).isEqualTo(0)
-    }
-
-    @Test
-    fun testAlertEntryCompareTo_ongoingCallLessThanActiveRemoteInput() {
-        val hum = createHeadsUpManager()
-
-        val ongoingCall =
-            hum.HeadsUpEntry(
-                NotificationEntryBuilder()
-                    .setSbn(
-                        HeadsUpManagerTestUtil.createSbn(
-                            /* id = */ 0,
-                            Notification.Builder(mContext, "")
-                                .setCategory(Notification.CATEGORY_CALL)
-                                .setOngoing(true),
-                        )
-                    )
-                    .build()
-            )
-
-        val activeRemoteInput =
-            hum.HeadsUpEntry(HeadsUpManagerTestUtil.createEntry(/* id= */ 1, mContext))
-        activeRemoteInput.mRemoteInputActive = true
-
-        assertThat(ongoingCall.compareTo(activeRemoteInput)).isLessThan(0)
-        assertThat(activeRemoteInput.compareTo(ongoingCall)).isGreaterThan(0)
-    }
-
-    @Test
-    fun testAlertEntryCompareTo_incomingCallLessThanActiveRemoteInput() {
-        val hum = createHeadsUpManager()
-
-        val person = Person.Builder().setName("person").build()
-        val intent = Mockito.mock(PendingIntent::class.java)
-        val incomingCall =
-            hum.HeadsUpEntry(
-                NotificationEntryBuilder()
-                    .setSbn(
-                        HeadsUpManagerTestUtil.createSbn(
-                            /* id = */ 0,
-                            Notification.Builder(mContext, "")
-                                .setStyle(
-                                    Notification.CallStyle.forIncomingCall(person, intent, intent)
-                                ),
-                        )
-                    )
-                    .build()
-            )
-
-        val activeRemoteInput =
-            hum.HeadsUpEntry(HeadsUpManagerTestUtil.createEntry(/* id= */ 1, mContext))
-        activeRemoteInput.mRemoteInputActive = true
-
-        assertThat(incomingCall.compareTo(activeRemoteInput)).isLessThan(0)
-        assertThat(activeRemoteInput.compareTo(incomingCall)).isGreaterThan(0)
-    }
-
-    @Test
-    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    fun testPinEntry_logsPeek_throttleEnabled() {
-        val hum = createHeadsUpManager()
-
-        // Needs full screen intent in order to be pinned
-        val entryToPin =
-            hum.HeadsUpEntry(
-                HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
-            )
-
-        // Note: the standard way to show a notification would be calling showNotification rather
-        // than onAlertEntryAdded. However, in practice showNotification in effect adds
-        // the notification and then updates it; in order to not log twice, the entry needs
-        // to have a functional ExpandableNotificationRow that can keep track of whether it's
-        // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit.
-        hum.onEntryAdded(entryToPin)
-
-        assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(2)
-        assertThat(AvalancheController.ThrottleEvent.AVALANCHE_THROTTLING_HUN_SHOWN.getId())
-            .isEqualTo(mUiEventLoggerFake.eventId(0))
-        assertThat(HeadsUpManagerImpl.NotificationPeekEvent.NOTIFICATION_PEEK.id)
-            .isEqualTo(mUiEventLoggerFake.eventId(1))
-    }
-
-    @Test
-    @DisableFlags(NotificationThrottleHun.FLAG_NAME)
-    fun testPinEntry_logsPeek_throttleDisabled() {
-        val hum = createHeadsUpManager()
-
-        // Needs full screen intent in order to be pinned
-        val entryToPin =
-            hum.HeadsUpEntry(
-                HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
-            )
-
-        // Note: the standard way to show a notification would be calling showNotification rather
-        // than onAlertEntryAdded. However, in practice showNotification in effect adds
-        // the notification and then updates it; in order to not log twice, the entry needs
-        // to have a functional ExpandableNotificationRow that can keep track of whether it's
-        // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit.
-        hum.onEntryAdded(entryToPin)
-
-        assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1)
-        assertThat(HeadsUpManagerImpl.NotificationPeekEvent.NOTIFICATION_PEEK.id)
-            .isEqualTo(mUiEventLoggerFake.eventId(0))
-    }
-
-    @Test
-    fun testSetUserActionMayIndirectlyRemove() {
-        val hum = createHeadsUpManager()
-        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
-
-        hum.showNotification(notifEntry)
-
-        assertThat(hum.canRemoveImmediately(notifEntry.key)).isFalse()
-
-        hum.setUserActionMayIndirectlyRemove(notifEntry)
-
-        assertThat(hum.canRemoveImmediately(notifEntry.key)).isTrue()
-    }
-
-    companion object {
-        const val TEST_TOUCH_ACCEPTANCE_TIME: Int = 200
-        const val TEST_A11Y_AUTO_DISMISS_TIME: Int = 1000
-        const val TEST_EXTENSION_TIME = 500
-
-        const val TEST_MINIMUM_DISPLAY_TIME: Int = 400
-        const val TEST_AUTO_DISMISS_TIME: Int = 600
-        const val TEST_STICKY_AUTO_DISMISS_TIME: Int = 800
-
-        // Number of notifications to use in tests requiring multiple notifications
-        private const val TEST_NUM_NOTIFICATIONS = 4
-
-        init {
-            Truth.assertThat(TEST_MINIMUM_DISPLAY_TIME).isLessThan(TEST_AUTO_DISMISS_TIME)
-            Truth.assertThat(TEST_AUTO_DISMISS_TIME).isLessThan(TEST_STICKY_AUTO_DISMISS_TIME)
-            Truth.assertThat(TEST_STICKY_AUTO_DISMISS_TIME).isLessThan(TEST_A11Y_AUTO_DISMISS_TIME)
-        }
-
-        @get:Parameters(name = "{0}")
-        @JvmStatic
-        val flags: List<FlagsParameterization>
-            get() = FlagsParameterization.allCombinationsOf(NotificationThrottleHun.FLAG_NAME)
-    }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
index a5fecb8..8420c49 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
@@ -15,25 +15,38 @@
  */
 package com.android.systemui.statusbar.notification.headsup
 
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Person
 import android.os.Handler
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
+import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
+import android.view.accessibility.accessibilityManager
 import android.view.accessibility.accessibilityManagerWrapper
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.uiEventLoggerFake
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.dump.dumpManager
+import com.android.systemui.flags.BrokenWithSceneContainer
 import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.kosmos.runTest
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.kosmos.useUnconfinedTestDispatcher
-import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.shadeTestUtil
 import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.collection.provider.visualStabilityProvider
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
+import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerImpl.HeadsUpEntry
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
 import com.android.systemui.statusbar.phone.keyguardBypassController
 import com.android.systemui.statusbar.policy.configurationController
@@ -41,12 +54,19 @@
 import com.android.systemui.testKosmos
 import com.android.systemui.util.concurrency.mockExecutorHandler
 import com.android.systemui.util.kotlin.JavaAdapter
+import com.android.systemui.util.settings.fakeGlobalSettings
+import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
 import platform.test.runner.parameterized.Parameters
@@ -54,19 +74,26 @@
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
 @RunWithLooper
-class HeadsUpManagerImplTest(flags: FlagsParameterization) : HeadsUpManagerImplOldTest(flags) {
-
-    private val headsUpManagerLogger = HeadsUpManagerLogger(logcatLogBuffer())
+class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() {
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
 
     private val kosmos = testKosmos().useUnconfinedTestDispatcher()
     private val testScope = kosmos.testScope
 
     private val groupManager = mock<GroupMembershipManager>()
     private val bgHandler = mock<Handler>()
+    private val headsUpManagerLogger = mock<HeadsUpManagerLogger>()
 
     val statusBarStateController = kosmos.sysuiStatusBarStateController
+    private val globalSettings = kosmos.fakeGlobalSettings
+    private val systemClock = kosmos.fakeSystemClock
+    private val executor = kosmos.fakeExecutor
+    private val uiEventLoggerFake = kosmos.uiEventLoggerFake
     private val javaAdapter: JavaAdapter = JavaAdapter(testScope.backgroundScope)
 
+    private lateinit var testHelper: NotificationTestHelper
     private lateinit var avalancheController: AvalancheController
     private lateinit var underTest: HeadsUpManagerImpl
 
@@ -90,12 +117,15 @@
             )
         }
 
+        allowTestableLooperAsMainThread()
+        testHelper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+
         whenever(kosmos.keyguardBypassController.bypassEnabled).thenReturn(false)
         kosmos.visualStabilityProvider.isReorderingAllowed = true
         avalancheController =
             AvalancheController(
                 kosmos.dumpManager,
-                kosmos.uiEventLoggerFake,
+                uiEventLoggerFake,
                 headsUpManagerLogger,
                 bgHandler,
             )
@@ -113,7 +143,7 @@
                 systemClock,
                 executor,
                 kosmos.accessibilityManagerWrapper,
-                kosmos.uiEventLoggerFake,
+                uiEventLoggerFake,
                 javaAdapter,
                 kosmos.shadeInteractor,
                 avalancheController,
@@ -121,6 +151,220 @@
     }
 
     @Test
+    fun testHasNotifications_headsUpManagerMapNotEmpty_true() {
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        underTest.showNotification(entry)
+
+        assertThat(underTest.mHeadsUpEntryMap).isNotEmpty()
+        assertThat(underTest.hasNotifications()).isTrue()
+    }
+
+    @Test
+    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+    fun testHasNotifications_avalancheMapNotEmpty_true() {
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val headsUpEntry = underTest.createHeadsUpEntry(notifEntry)
+        avalancheController.addToNext(headsUpEntry) {}
+
+        assertThat(avalancheController.getWaitingEntryList()).isNotEmpty()
+        assertThat(underTest.hasNotifications()).isTrue()
+    }
+
+    @Test
+    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+    fun testHasNotifications_false() {
+        assertThat(underTest.mHeadsUpEntryMap).isEmpty()
+        assertThat(avalancheController.getWaitingEntryList()).isEmpty()
+        assertThat(underTest.hasNotifications()).isFalse()
+    }
+
+    @Test
+    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+    fun testGetHeadsUpEntryList_includesAvalancheEntryList() {
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val headsUpEntry = underTest.createHeadsUpEntry(notifEntry)
+        avalancheController.addToNext(headsUpEntry) {}
+
+        assertThat(underTest.headsUpEntryList).contains(headsUpEntry)
+    }
+
+    @Test
+    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+    fun testGetHeadsUpEntry_returnsAvalancheEntry() {
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val headsUpEntry = underTest.createHeadsUpEntry(notifEntry)
+        avalancheController.addToNext(headsUpEntry) {}
+
+        assertThat(underTest.getHeadsUpEntry(notifEntry.key)).isEqualTo(headsUpEntry)
+    }
+
+    @Test
+    fun testShowNotification_addsEntry() {
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        underTest.showNotification(entry)
+
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+        assertThat(underTest.hasNotifications()).isTrue()
+        assertThat(underTest.getEntry(entry.key)).isEqualTo(entry)
+    }
+
+    @Test
+    fun testShowNotification_autoDismisses() {
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        underTest.showNotification(entry)
+        systemClock.advanceTime((TEST_AUTO_DISMISS_TIME * 3 / 2).toLong())
+
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
+    }
+
+    @Test
+    fun testRemoveNotification_removeDeferred() {
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        underTest.showNotification(entry)
+
+        val removedImmediately =
+            underTest.removeNotification(
+                entry.key,
+                /* releaseImmediately= */ false,
+                "removeDeferred",
+            )
+        assertThat(removedImmediately).isFalse()
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+    }
+
+    @Test
+    fun testRemoveNotification_forceRemove() {
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        underTest.showNotification(entry)
+
+        val removedImmediately =
+            underTest.removeNotification(entry.key, /* releaseImmediately= */ true, "forceRemove")
+        assertThat(removedImmediately).isTrue()
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
+    }
+
+    @Test
+    fun testReleaseAllImmediately() {
+        for (i in 0 until 4) {
+            val entry = HeadsUpManagerTestUtil.createEntry(i, mContext)
+            entry.row = mock<ExpandableNotificationRow>()
+            underTest.showNotification(entry)
+        }
+
+        underTest.releaseAllImmediately()
+
+        assertThat(underTest.allEntries.count()).isEqualTo(0)
+    }
+
+    @Test
+    fun testCanRemoveImmediately_notShownLongEnough() {
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        underTest.showNotification(entry)
+
+        // The entry has just been added so we should not remove immediately.
+        assertThat(underTest.canRemoveImmediately(entry.key)).isFalse()
+    }
+
+    @Test
+    fun testHunRemovedLogging() {
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val headsUpEntry = underTest.HeadsUpEntry(notifEntry)
+        headsUpEntry.setRowPinnedStatus(PinnedStatus.NotPinned)
+
+        underTest.onEntryRemoved(headsUpEntry, "test")
+
+        verify(headsUpManagerLogger, times(1)).logNotificationActuallyRemoved(eq(notifEntry))
+    }
+
+    @Test
+    fun testShowNotification_autoDismissesIncludingTouchAcceptanceDelay() {
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        useAccessibilityTimeout(false)
+
+        underTest.showNotification(entry)
+        systemClock.advanceTime((TEST_TOUCH_ACCEPTANCE_TIME / 2 + TEST_AUTO_DISMISS_TIME).toLong())
+
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+    }
+
+    @Test
+    fun testShowNotification_autoDismissesWithDefaultTimeout() {
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        useAccessibilityTimeout(false)
+
+        underTest.showNotification(entry)
+        systemClock.advanceTime(
+            (TEST_TOUCH_ACCEPTANCE_TIME +
+                    (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
+                .toLong()
+        )
+
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
+    }
+
+    @Test
+    fun testRemoveNotification_beforeMinimumDisplayTime() {
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        useAccessibilityTimeout(false)
+
+        underTest.showNotification(entry)
+
+        val removedImmediately =
+            underTest.removeNotification(
+                entry.key,
+                /* releaseImmediately = */ false,
+                "beforeMinimumDisplayTime",
+            )
+        assertThat(removedImmediately).isFalse()
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+
+        systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong())
+
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
+    }
+
+    @Test
+    fun testRemoveNotification_afterMinimumDisplayTime() {
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        useAccessibilityTimeout(false)
+
+        underTest.showNotification(entry)
+        systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong())
+
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+
+        val removedImmediately =
+            underTest.removeNotification(
+                entry.key,
+                /* releaseImmediately = */ false,
+                "afterMinimumDisplayTime",
+            )
+        assertThat(removedImmediately).isTrue()
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
+    }
+
+    @Test
+    fun testRemoveNotification_releaseImmediately() {
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        underTest.showNotification(entry)
+
+        val removedImmediately =
+            underTest.removeNotification(
+                entry.key,
+                /* releaseImmediately = */ true,
+                "afterMinimumDisplayTime",
+            )
+        assertThat(removedImmediately).isTrue()
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse()
+    }
+
+    @Test
     fun testSnooze() {
         val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
         underTest.showNotification(entry)
@@ -160,7 +404,7 @@
     fun testCanRemoveImmediately_notTopEntry() {
         val earlierEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
         val laterEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 1, mContext)
-        laterEntry.row = mRow
+        laterEntry.row = mock<ExpandableNotificationRow>()
         underTest.showNotification(earlierEntry)
         underTest.showNotification(laterEntry)
 
@@ -226,6 +470,122 @@
     }
 
     @Test
+    fun testShowNotification_sticky_neverAutoDismisses() {
+        val entry = createStickyEntry(id = 0)
+        useAccessibilityTimeout(false)
+
+        underTest.showNotification(entry)
+        systemClock.advanceTime(
+            (TEST_TOUCH_ACCEPTANCE_TIME + 2 * TEST_A11Y_AUTO_DISMISS_TIME).toLong()
+        )
+
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+    }
+
+    @Test
+    fun testShowNotification_autoDismissesWithAccessibilityTimeout() {
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        useAccessibilityTimeout(true)
+
+        underTest.showNotification(entry)
+        systemClock.advanceTime(
+            (TEST_TOUCH_ACCEPTANCE_TIME +
+                    (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
+                .toLong()
+        )
+
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+    }
+
+    @Test
+    fun testShowNotification_stickyForSomeTime_autoDismissesWithStickyTimeout() {
+        val entry = createStickyForSomeTimeEntry(id = 0)
+        useAccessibilityTimeout(false)
+
+        underTest.showNotification(entry)
+        systemClock.advanceTime(
+            (TEST_TOUCH_ACCEPTANCE_TIME +
+                    (TEST_AUTO_DISMISS_TIME + TEST_STICKY_AUTO_DISMISS_TIME) / 2)
+                .toLong()
+        )
+
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+    }
+
+    @Test
+    fun testShowNotification_stickyForSomeTime_autoDismissesWithAccessibilityTimeout() {
+        val entry = createStickyForSomeTimeEntry(id = 0)
+        useAccessibilityTimeout(true)
+
+        underTest.showNotification(entry)
+        systemClock.advanceTime(
+            (TEST_TOUCH_ACCEPTANCE_TIME +
+                    (TEST_STICKY_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
+                .toLong()
+        )
+
+        assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue()
+    }
+
+    @Test
+    fun testIsSticky_rowPinnedAndExpanded_true() {
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val row = testHelper.createRow()
+        row.setPinnedStatus(PinnedStatus.PinnedBySystem)
+        notifEntry.row = row
+
+        underTest.showNotification(notifEntry)
+
+        val headsUpEntry = underTest.getHeadsUpEntry(notifEntry.key)
+        headsUpEntry!!.setExpanded(true)
+
+        assertThat(underTest.isSticky(notifEntry.key)).isTrue()
+    }
+
+    @Test
+    fun testIsSticky_remoteInputActive_true() {
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        underTest.showNotification(notifEntry)
+
+        val headsUpEntry = underTest.getHeadsUpEntry(notifEntry.key)
+        headsUpEntry!!.mRemoteInputActive = true
+
+        assertThat(underTest.isSticky(notifEntry.key)).isTrue()
+    }
+
+    @Test
+    fun testIsSticky_hasFullScreenIntent_true() {
+        val notifEntry = HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
+
+        underTest.showNotification(notifEntry)
+
+        assertThat(underTest.isSticky(notifEntry.key)).isTrue()
+    }
+
+    @Test
+    fun testIsSticky_stickyForSomeTime_false() {
+        val entry = createStickyForSomeTimeEntry(id = 0)
+
+        underTest.showNotification(entry)
+
+        assertThat(underTest.isSticky(entry.key)).isFalse()
+    }
+
+    @Test
+    fun testIsSticky_false() {
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        underTest.showNotification(notifEntry)
+
+        val headsUpEntry = underTest.getHeadsUpEntry(notifEntry.key)
+        headsUpEntry!!.setExpanded(false)
+        headsUpEntry.mRemoteInputActive = false
+
+        assertThat(underTest.isSticky(notifEntry.key)).isFalse()
+    }
+
+    @Test
     fun testShouldHeadsUpBecomePinned_noFSI_false() =
         kosmos.runTest {
             statusBarStateController.setState(StatusBarState.KEYGUARD)
@@ -270,11 +630,13 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(381869885) // because `ShadeTestUtil.setShadeExpansion(0f)`
+    // still causes `ShadeInteractor.isAnyExpanded` to emit `true`, when it should emit `false`.
     fun shouldHeadsUpBecomePinned_shadeNotExpanded_true() =
         kosmos.runTest {
             // GIVEN
-            shadeTestUtil.setShadeExpansion(0f)
-            // TODO(b/381869885): Determine why we need both of these ShadeTestUtil calls.
+            // TODO(b/381869885): We should be able to use `ShadeTestUtil.setShadeExpansion(0f)`
+            // instead.
             shadeTestUtil.setLegacyExpandedOrAwaitingInputTransfer(false)
 
             val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
@@ -347,8 +709,183 @@
             assertThat(underTest.shouldHeadsUpBecomePinned(entry)).isFalse()
         }
 
+    @Test
+    fun testCompareTo_withNullEntries() {
+        val alertEntry = NotificationEntryBuilder().setTag("alert").build()
+
+        underTest.showNotification(alertEntry)
+
+        assertThat(underTest.compare(alertEntry, null)).isLessThan(0)
+        assertThat(underTest.compare(null, alertEntry)).isGreaterThan(0)
+        assertThat(underTest.compare(null, null)).isEqualTo(0)
+    }
+
+    @Test
+    fun testCompareTo_withNonAlertEntries() {
+        val nonAlertEntry1 = NotificationEntryBuilder().setTag("nae1").build()
+        val nonAlertEntry2 = NotificationEntryBuilder().setTag("nae2").build()
+        val alertEntry = NotificationEntryBuilder().setTag("alert").build()
+        underTest.showNotification(alertEntry)
+
+        assertThat(underTest.compare(alertEntry, nonAlertEntry1)).isLessThan(0)
+        assertThat(underTest.compare(nonAlertEntry1, alertEntry)).isGreaterThan(0)
+        assertThat(underTest.compare(nonAlertEntry1, nonAlertEntry2)).isEqualTo(0)
+    }
+
+    @Test
+    fun testAlertEntryCompareTo_ongoingCallLessThanActiveRemoteInput() {
+        val ongoingCall =
+            underTest.HeadsUpEntry(
+                NotificationEntryBuilder()
+                    .setSbn(
+                        HeadsUpManagerTestUtil.createSbn(
+                            /* id = */ 0,
+                            Notification.Builder(mContext, "")
+                                .setCategory(Notification.CATEGORY_CALL)
+                                .setOngoing(true),
+                        )
+                    )
+                    .build()
+            )
+
+        val activeRemoteInput =
+            underTest.HeadsUpEntry(HeadsUpManagerTestUtil.createEntry(/* id= */ 1, mContext))
+        activeRemoteInput.mRemoteInputActive = true
+
+        assertThat(ongoingCall.compareTo(activeRemoteInput)).isLessThan(0)
+        assertThat(activeRemoteInput.compareTo(ongoingCall)).isGreaterThan(0)
+    }
+
+    @Test
+    fun testAlertEntryCompareTo_incomingCallLessThanActiveRemoteInput() {
+        val person = Person.Builder().setName("person").build()
+        val intent = mock<PendingIntent>()
+        val incomingCall =
+            underTest.HeadsUpEntry(
+                NotificationEntryBuilder()
+                    .setSbn(
+                        HeadsUpManagerTestUtil.createSbn(
+                            /* id = */ 0,
+                            Notification.Builder(mContext, "")
+                                .setStyle(
+                                    Notification.CallStyle.forIncomingCall(person, intent, intent)
+                                ),
+                        )
+                    )
+                    .build()
+            )
+
+        val activeRemoteInput =
+            underTest.HeadsUpEntry(HeadsUpManagerTestUtil.createEntry(/* id= */ 1, mContext))
+        activeRemoteInput.mRemoteInputActive = true
+
+        assertThat(incomingCall.compareTo(activeRemoteInput)).isLessThan(0)
+        assertThat(activeRemoteInput.compareTo(incomingCall)).isGreaterThan(0)
+    }
+
+    @Test
+    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+    fun testPinEntry_logsPeek_throttleEnabled() {
+        // Needs full screen intent in order to be pinned
+        val entryToPin =
+            underTest.HeadsUpEntry(
+                HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
+            )
+
+        // Note: the standard way to show a notification would be calling showNotification rather
+        // than onAlertEntryAdded. However, in practice showNotification in effect adds
+        // the notification and then updates it; in order to not log twice, the entry needs
+        // to have a functional ExpandableNotificationRow that can keep track of whether it's
+        // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit.
+        underTest.onEntryAdded(entryToPin)
+
+        assertThat(uiEventLoggerFake.numLogs()).isEqualTo(2)
+        assertThat(AvalancheController.ThrottleEvent.AVALANCHE_THROTTLING_HUN_SHOWN.getId())
+            .isEqualTo(uiEventLoggerFake.eventId(0))
+        assertThat(HeadsUpManagerImpl.NotificationPeekEvent.NOTIFICATION_PEEK.id)
+            .isEqualTo(uiEventLoggerFake.eventId(1))
+    }
+
+    @Test
+    @DisableFlags(NotificationThrottleHun.FLAG_NAME)
+    fun testPinEntry_logsPeek_throttleDisabled() {
+        // Needs full screen intent in order to be pinned
+        val entryToPin =
+            underTest.HeadsUpEntry(
+                HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
+            )
+
+        // Note: the standard way to show a notification would be calling showNotification rather
+        // than onAlertEntryAdded. However, in practice showNotification in effect adds
+        // the notification and then updates it; in order to not log twice, the entry needs
+        // to have a functional ExpandableNotificationRow that can keep track of whether it's
+        // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit.
+        underTest.onEntryAdded(entryToPin)
+
+        assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
+        assertThat(HeadsUpManagerImpl.NotificationPeekEvent.NOTIFICATION_PEEK.id)
+            .isEqualTo(uiEventLoggerFake.eventId(0))
+    }
+
+    @Test
+    fun testSetUserActionMayIndirectlyRemove() {
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        underTest.showNotification(notifEntry)
+
+        assertThat(underTest.canRemoveImmediately(notifEntry.key)).isFalse()
+
+        underTest.setUserActionMayIndirectlyRemove(notifEntry)
+
+        assertThat(underTest.canRemoveImmediately(notifEntry.key)).isTrue()
+    }
+
+    private fun createStickyEntry(id: Int): NotificationEntry {
+        val notif =
+            Notification.Builder(mContext, "")
+                .setSmallIcon(R.drawable.ic_person)
+                .setFullScreenIntent(mock<PendingIntent>(), /* highPriority= */ true)
+                .build()
+        return HeadsUpManagerTestUtil.createEntry(id, notif)
+    }
+
+    private fun createStickyForSomeTimeEntry(id: Int): NotificationEntry {
+        val notif =
+            Notification.Builder(mContext, "")
+                .setSmallIcon(R.drawable.ic_person)
+                .setFlag(Notification.FLAG_FSI_REQUESTED_BUT_DENIED, true)
+                .build()
+        return HeadsUpManagerTestUtil.createEntry(id, notif)
+    }
+
+    private fun useAccessibilityTimeout(use: Boolean) {
+        if (use) {
+            whenever(kosmos.accessibilityManager.getRecommendedTimeoutMillis(any(), any()))
+                .thenReturn(TEST_A11Y_AUTO_DISMISS_TIME)
+        } else {
+            doAnswer { it.getArgument(0) as Int }
+                .whenever(kosmos.accessibilityManager)
+                .getRecommendedTimeoutMillis(any(), any())
+        }
+    }
+
     companion object {
+        const val TEST_TOUCH_ACCEPTANCE_TIME = 200
+        const val TEST_A11Y_AUTO_DISMISS_TIME = 1000
+        const val TEST_EXTENSION_TIME = 500
+
+        const val TEST_MINIMUM_DISPLAY_TIME = 400
+        const val TEST_AUTO_DISMISS_TIME = 600
+        const val TEST_STICKY_AUTO_DISMISS_TIME = 800
+
+        init {
+            assertThat(TEST_MINIMUM_DISPLAY_TIME).isLessThan(TEST_AUTO_DISMISS_TIME)
+            assertThat(TEST_AUTO_DISMISS_TIME).isLessThan(TEST_STICKY_AUTO_DISMISS_TIME)
+            assertThat(TEST_STICKY_AUTO_DISMISS_TIME).isLessThan(TEST_A11Y_AUTO_DISMISS_TIME)
+        }
+
         @get:Parameters(name = "{0}")
+        @JvmStatic
         val flags: List<FlagsParameterization>
             get() = buildList {
                 addAll(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
index c0a206a..9ad2315 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
@@ -49,11 +49,7 @@
     private val dispatcher = StandardTestDispatcher()
     private val testScope = TestScope(dispatcher)
 
-    private val iconsInteractor =
-        FakeMobileIconsInteractor(
-            FakeMobileMappingsProxy(),
-            mock(),
-        )
+    private val iconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
 
     private val repo = FakeDeviceBasedSatelliteRepository()
     private val connectivityRepository = FakeConnectivityRepository()
@@ -515,7 +511,7 @@
 
             // GIVEN, 2 connection
             val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
-            val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+            val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2)
 
             // WHEN all connections are NOT OOS.
             i1.isInService.value = true
@@ -547,7 +543,7 @@
             // GIVEN a condition that should return true (all conections OOS)
 
             val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
-            val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+            val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2)
 
             i1.isInService.value = true
             i2.isInService.value = true
@@ -579,4 +575,40 @@
             // THEN the interactor returns true due to the wifi network being active
             assertThat(latest).isTrue()
         }
+
+    @Test
+    @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+    fun isAnyConnectionNtn_trueWhenAnyNtn() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isAnyConnectionNtn)
+
+            // GIVEN, 2 connection
+            val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+            val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2)
+
+            // WHEN at least one connection is using ntn
+            i1.isNonTerrestrial.value = true
+            i2.isNonTerrestrial.value = false
+
+            // THEN the value is propagated to this interactor
+            assertThat(latest).isTrue()
+        }
+
+    @Test
+    @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+    fun isAnyConnectionNtn_falseWhenNoNtn() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isAnyConnectionNtn)
+
+            // GIVEN, 2 connection
+            val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+            val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2)
+
+            // WHEN at no connection is using ntn
+            i1.isNonTerrestrial.value = false
+            i2.isNonTerrestrial.value = false
+
+            // THEN the value is propagated to this interactor
+            assertThat(latest).isFalse()
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
index 509aa7a..fe5b56a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
@@ -327,10 +327,11 @@
             // GIVEN satellite is allowed
             repo.isSatelliteAllowedForCurrentLocation.value = true
 
-            // GIVEN all icons are OOS
+            // GIVEN all icons are OOS and not ntn
             val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
             i1.isInService.value = false
             i1.isEmergencyOnly.value = false
+            i1.isNonTerrestrial.value = false
 
             // GIVEN apm is disabled
             airplaneModeRepository.setIsAirplaneMode(false)
@@ -344,6 +345,29 @@
         }
 
     @Test
+    fun icon_nullWhenConnected_mobileNtnConnectionExists() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.icon)
+
+            // GIVEN satellite is allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+
+            // GIVEN ntn connection exists
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isNonTerrestrial.value = true
+
+            // GIVEN apm is disabled
+            airplaneModeRepository.setIsAirplaneMode(false)
+
+            // GIVEN satellite reports that it is Connected
+            repo.connectionState.value = SatelliteConnectionState.On
+
+            // THEN icon is null because despite being connected, the mobile stack is reporting a
+            // nonTerrestrial network, and therefore will have its own icon
+            assertThat(latest).isNull()
+        }
+
+    @Test
     fun icon_satelliteIsProvisioned() =
         testScope.runTest {
             val latest by collectLastValue(underTest.icon)
diff --git a/packages/SystemUI/res/layout/volume_dialog_slider.xml b/packages/SystemUI/res/layout/volume_dialog_slider.xml
index c1852b1..9ac456c 100644
--- a/packages/SystemUI/res/layout/volume_dialog_slider.xml
+++ b/packages/SystemUI/res/layout/volume_dialog_slider.xml
@@ -14,15 +14,21 @@
      limitations under the License.
 -->
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="@dimen/volume_dialog_slider_width"
-    android:layout_height="@dimen/volume_dialog_slider_height">
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
 
     <com.google.android.material.slider.Slider
-        style="@style/SystemUI.Material3.Slider.Volume"
         android:id="@+id/volume_dialog_slider"
-        android:layout_width="@dimen/volume_dialog_slider_height"
-        android:layout_height="match_parent"
+        style="@style/SystemUI.Material3.Slider.Volume"
+        android:layout_width="@dimen/volume_dialog_slider_width"
+        android:layout_height="@dimen/volume_dialog_slider_height"
         android:layout_gravity="center"
-        android:rotation="270"
-        android:theme="@style/Theme.Material3.Light" />
-</FrameLayout>
+        android:theme="@style/Theme.Material3.Light"
+        android:orientation="vertical"
+        app:thumbHeight="52dp"
+        app:trackCornerSize="12dp"
+        app:trackHeight="40dp"
+        app:trackStopIndicatorSize="6dp"
+        app:trackInsideCornerSize="2dp" />
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
index b2d02ed..a31e61f 100644
--- a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
@@ -107,6 +107,7 @@
                 activityOptions.setDisallowEnterPictureInPictureWhileLaunching(true)
                 activityOptions.rotationAnimationHint =
                     WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS
+                intent.collectExtraIntentKeys()
                 try {
                     activityTaskManager.startActivityAsUser(
                         null,
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
index 7242770..e264635 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
@@ -21,6 +21,9 @@
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.contextualeducation.GestureType
 import com.android.systemui.contextualeducation.GestureType.ALL_APPS
+import com.android.systemui.contextualeducation.GestureType.BACK
+import com.android.systemui.contextualeducation.GestureType.HOME
+import com.android.systemui.contextualeducation.GestureType.OVERVIEW
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.education.ContextualEducationMetricsLogger
@@ -37,6 +40,7 @@
 import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
 import java.time.Clock
+import java.time.Instant
 import javax.inject.Inject
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.days
@@ -48,6 +52,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.merge
@@ -71,6 +76,8 @@
         const val TAG = "KeyboardTouchpadEduInteractor"
         const val MAX_SIGNAL_COUNT: Int = 2
         const val MAX_EDUCATION_SHOW_COUNT: Int = 2
+        const val MAX_TOAST_PER_USAGE_SESSION: Int = 2
+
         val usageSessionDuration =
             getDurationForConfig("persist.contextual_edu.usage_session_sec", 3.days)
         val minIntervalBetweenEdu =
@@ -110,6 +117,16 @@
         awaitClose { overviewProxyService.removeCallback(listener) }
     }
 
+    private val gestureModelMap: Flow<Map<GestureType, GestureEduModel>> =
+        combine(
+            contextualEducationInteractor.backGestureModelFlow,
+            contextualEducationInteractor.homeGestureModelFlow,
+            contextualEducationInteractor.overviewGestureModelFlow,
+            contextualEducationInteractor.allAppsGestureModelFlow,
+        ) { back, home, overview, allApps ->
+            mapOf(BACK to back, HOME to home, OVERVIEW to overview, ALL_APPS to allApps)
+        }
+
     @OptIn(ExperimentalCoroutinesApi::class)
     override fun start() {
         backgroundScope.launch {
@@ -211,7 +228,11 @@
 
     private suspend fun incrementSignalCount(gestureType: GestureType) {
         val targetDevice = getTargetDevice(gestureType)
-        if (isTargetDeviceConnected(targetDevice) && hasInitialDelayElapsed(targetDevice)) {
+        if (
+            isTargetDeviceConnected(targetDevice) &&
+                hasInitialDelayElapsed(targetDevice) &&
+                isMinIntervalForToastEduElapsed(gestureType)
+        ) {
             contextualEducationInteractor.incrementSignalCount(gestureType)
         }
     }
@@ -223,6 +244,28 @@
         }
     }
 
+    private suspend fun isMinIntervalForToastEduElapsed(gestureType: GestureType): Boolean {
+        val gestureModelMap = gestureModelMap.first()
+        // Only perform checking if the next edu is toast (i.e. no education is shown yet)
+        if (gestureModelMap[gestureType]?.educationShownCount != 0) {
+            return true
+        }
+
+        val wasLastEduToast = { gesture: GestureEduModel -> gesture.educationShownCount == 1 }
+        val toastEduTimesInCurrentSession: List<Instant> =
+            gestureModelMap.values
+                .filter { wasLastEduToast(it) }
+                .mapNotNull { it.lastEducationTime }
+                .filter { it >= clock.instant().minusSeconds(usageSessionDuration.inWholeSeconds) }
+
+        return if (toastEduTimesInCurrentSession.size >= MAX_TOAST_PER_USAGE_SESSION) {
+            val lastToastTime: Instant? = toastEduTimesInCurrentSession.maxOrNull()
+            clock.instant().isAfter(lastToastTime?.plusSeconds(usageSessionDuration.inWholeSeconds))
+        } else {
+            true
+        }
+    }
+
     /**
      * Keyboard shortcut education would be provided for All Apps. Touchpad gesture education would
      * be provided for the rest of the gesture types (i.e. Home, Overview, Back). This method maps
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
index 17b78eb..e8c4274 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.qs.tiles.base.interactor
 
 import android.annotation.WorkerThread
+import com.android.systemui.plugins.qs.TileDetailsViewModel
 
 interface QSTileUserActionInteractor<DATA_TYPE> {
     /**
@@ -27,4 +28,17 @@
      * It's safe to run long running computations inside this function.
      */
     @WorkerThread suspend fun handleInput(input: QSTileInput<DATA_TYPE>)
+
+    /**
+     * Provides the [TileDetailsViewModel] for constructing the corresponding details view.
+     *
+     * This property is defined here to reuse the business logic. For example, reusing the user
+     * long-click as the go-to-settings callback in the details view.
+     * Subclasses can override this property to provide a specific [TileDetailsViewModel]
+     * implementation.
+     *
+     * @return The [TileDetailsViewModel] instance, or null if not implemented.
+     */
+    val detailsViewModel: TileDetailsViewModel?
+        get() = null
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
index aeb6cef..224fa10 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
@@ -20,6 +20,7 @@
 import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.Dumpable
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.qs.TileDetailsViewModel
 import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
 import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
 import com.android.systemui.qs.tiles.base.interactor.DisabledByPolicyInteractor
@@ -115,6 +116,9 @@
             .flowOn(backgroundDispatcher)
             .stateIn(tileScope, SharingStarted.WhileSubscribed(), true)
 
+    override val detailsViewModel: TileDetailsViewModel?
+        get() = userActionInteractor().detailsViewModel
+
     override fun forceUpdate() {
         tileScope.launch(context = backgroundDispatcher) { forceUpdates.emit(Unit) }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
index a963b28..fdb15b9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
@@ -18,15 +18,23 @@
 
 import android.content.Intent
 import android.provider.Settings
+import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.qs.TileDetailsViewModel
 import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
 import com.android.systemui.qs.tiles.base.interactor.QSTileInput
 import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel
 import com.android.systemui.qs.tiles.dialog.InternetDialogManager
 import com.android.systemui.qs.tiles.dialog.WifiStateWorker
 import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
 import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
 import com.android.systemui.statusbar.connectivity.AccessPointController
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.stateIn
 import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.withContext
@@ -61,11 +69,18 @@
                     wifiStateWorker.isWifiEnabled = !wifiStateWorker.isWifiEnabled
                 }
                 is QSTileUserAction.LongClick -> {
-                    qsTileIntentUserActionHandler.handle(
-                        action.expandable,
-                        Intent(Settings.ACTION_WIFI_SETTINGS)
-                    )
+                    handleLongClick(action.expandable)
                 }
             }
         }
+
+    override val detailsViewModel: TileDetailsViewModel =
+        InternetDetailsViewModel { handleLongClick(null) }
+
+    private fun handleLongClick(expandable:Expandable?){
+        qsTileIntentUserActionHandler.handle(
+            expandable,
+            Intent(Settings.ACTION_WIFI_SETTINGS)
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
index b1b0001..e8b9926 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.qs.tiles.viewmodel
 
 import android.os.UserHandle
+import com.android.systemui.plugins.qs.TileDetailsViewModel
 import kotlinx.coroutines.flow.StateFlow
 
 /**
@@ -37,6 +38,10 @@
     /** Specifies whether this device currently supports this tile. */
     val isAvailable: StateFlow<Boolean>
 
+    /** Specifies the [TileDetailsViewModel] for constructing the corresponding details view. */
+    val detailsViewModel: TileDetailsViewModel?
+        get() = null
+
     /**
      * Notifies about the user change. Implementations should avoid using 3rd party userId sources
      * and use this value instead. This is to maintain consistent and concurrency-free behaviour
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 9d902d3..632eeef 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.UiBackground
 import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.plugins.qs.TileDetailsViewModel
 import com.android.systemui.qs.QSHost
 import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon
 import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIconWithRes
@@ -154,6 +155,10 @@
         qsTileViewModel.onUserChanged(UserHandle.of(currentUser))
     }
 
+    override fun getDetailsViewModel(): TileDetailsViewModel? {
+        return qsTileViewModel.detailsViewModel
+    }
+
     @Deprecated(
         "Not needed as {@link com.android.internal.logging.UiEvent} will use #getMetricsSpec",
         replaceWith = ReplaceWith("getMetricsSpec"),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
index d1338ea..f2ef2f0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
@@ -313,6 +313,7 @@
                     // if it is volume panel.
                     options.setDisallowEnterPictureInPictureWhileLaunching(true)
                 }
+                intent.collectExtraIntentKeys()
                 try {
                     result[0] =
                         ActivityTaskManager.getService()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
index 1cca3ae..d7cc65d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
@@ -180,6 +180,7 @@
                     // if it is volume panel.
                     options.setDisallowEnterPictureInPictureWhileLaunching(true)
                 }
+                intent.collectExtraIntentKeys()
                 try {
                     result[0] =
                         ActivityTaskManager.getService()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
index 08a98c3..12f578c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
@@ -161,6 +161,13 @@
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), true)
 
+    /** True if any known mobile network is currently using a non terrestrial network */
+    val isAnyConnectionNtn =
+        iconsInteractor.icons.aggregateOver(selector = { it.isNonTerrestrial }, false) {
+            nonTerrestrialNetworks ->
+            nonTerrestrialNetworks.any { it == true }
+        }
+
     companion object {
         const val TAG = "DeviceBasedSatelliteInteractor"
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
index f3d5139..ea915ef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
@@ -114,34 +114,39 @@
 
     private val showIcon =
         if (interactor.isOpportunisticSatelliteIconEnabled) {
-            canShowIcon
-                .flatMapLatest { canShow ->
-                    if (!canShow) {
-                        flowOf(false)
-                    } else {
-                        combine(
-                            shouldShowIconForOosAfterHysteresis,
-                            interactor.connectionState,
-                            interactor.isWifiActive,
-                            airplaneModeRepository.isAirplaneMode,
-                        ) { showForOos, connectionState, isWifiActive, isAirplaneMode ->
-                            if (isWifiActive || isAirplaneMode) {
-                                false
-                            } else {
-                                showForOos ||
-                                    connectionState == SatelliteConnectionState.On ||
-                                    connectionState == SatelliteConnectionState.Connected
+                canShowIcon
+                    .flatMapLatest { canShow ->
+                        if (!canShow) {
+                            flowOf(false)
+                        } else {
+                            combine(
+                                shouldShowIconForOosAfterHysteresis,
+                                interactor.isAnyConnectionNtn,
+                                interactor.connectionState,
+                                interactor.isWifiActive,
+                                airplaneModeRepository.isAirplaneMode,
+                            ) { showForOos, anyNtn, connectionState, isWifiActive, isAirplaneMode ->
+                                // anyNtn means that there is some mobile network using ntn, and the
+                                // mobile icon will show its own satellite icon
+                                if (isWifiActive || isAirplaneMode || anyNtn) {
+                                    false
+                                } else {
+                                    // Show for out of service (which has a hysteresis), or ignore
+                                    // the hysteresis if we're already connected
+                                    showForOos ||
+                                        connectionState == SatelliteConnectionState.On ||
+                                        connectionState == SatelliteConnectionState.Connected
+                                }
                             }
                         }
                     }
-                }
-                .distinctUntilChanged()
-                .logDiffsForTable(
-                    tableLog,
-                    columnPrefix = "vm",
-                    columnName = COL_VISIBLE,
-                    initialValue = false,
-                )
+                    .distinctUntilChanged()
+                    .logDiffsForTable(
+                        tableLog,
+                        columnPrefix = "vm",
+                        columnName = COL_VISIBLE,
+                        initialValue = false,
+                    )
             } else {
                 flowOf(false)
             }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
index b83613b..4071918 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
@@ -20,7 +20,6 @@
 import android.media.AudioManager.RINGER_MODE_NORMAL
 import android.media.AudioManager.RINGER_MODE_SILENT
 import android.media.AudioManager.RINGER_MODE_VIBRATE
-import android.provider.Settings
 import com.android.settingslib.volume.data.repository.AudioSystemRepository
 import com.android.settingslib.volume.shared.model.RingerMode
 import com.android.systemui.plugins.VolumeDialogController
@@ -66,11 +65,6 @@
                             }
                         },
                 currentRingerMode = RingerMode(state.ringerModeInternal),
-                isEnabled =
-                    !(state.zenMode == Settings.Global.ZEN_MODE_ALARMS ||
-                        state.zenMode == Settings.Global.ZEN_MODE_NO_INTERRUPTIONS ||
-                        (state.zenMode == Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS &&
-                            state.disallowRinger)),
                 isMuted = it.level == 0 || it.muted,
                 level = it.level,
                 levelMax = it.levelMax,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/shared/model/VolumeDialogRingerModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/shared/model/VolumeDialogRingerModel.kt
index 3c24e02..84a8280 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/shared/model/VolumeDialogRingerModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/shared/model/VolumeDialogRingerModel.kt
@@ -23,8 +23,6 @@
     val availableModes: List<RingerMode>,
     /** Current ringer mode internal */
     val currentRingerMode: RingerMode,
-    /** whether the ringer is allowed given the current ZenMode */
-    val isEnabled: Boolean,
     /** Whether the current ring stream level is zero or the controller state is muted */
     val isMuted: Boolean,
     /** Ring stream level */
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
index 3bd2721..9eee91b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.util.children
 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.ringer.ui.util.VolumeDialogRingerDrawerTransitionListener
 import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerButtonUiModel
 import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerButtonViewModel
 import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerDrawerState
@@ -42,6 +43,7 @@
 import com.android.systemui.volume.dialog.ringer.ui.viewmodel.VolumeDialogRingerDrawerViewModel
 import com.android.systemui.volume.dialog.ui.utils.suspendAnimate
 import javax.inject.Inject
+import kotlin.properties.Delegates
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.launchIn
@@ -71,6 +73,27 @@
         val drawerContainer = view.requireViewById<MotionLayout>(R.id.volume_ringer_drawer)
         val unselectedButtonUiModel = RingerButtonUiModel.getUnselectedButton(view.context)
         val selectedButtonUiModel = RingerButtonUiModel.getSelectedButton(view.context)
+        val volumeDialogBgSmallRadius =
+            view.context.resources.getDimensionPixelSize(
+                R.dimen.volume_dialog_background_square_corner_radius
+            )
+        val volumeDialogBgFullRadius =
+            view.context.resources.getDimensionPixelSize(
+                R.dimen.volume_dialog_background_corner_radius
+            )
+        var backgroundAnimationProgress: Float by
+            Delegates.observable(0F) { _, _, progress ->
+                volumeDialogBackgroundView.applyCorners(
+                    fullRadius = volumeDialogBgFullRadius,
+                    diff = volumeDialogBgFullRadius - volumeDialogBgSmallRadius,
+                    progress,
+                )
+            }
+        val ringerDrawerTransitionListener = VolumeDialogRingerDrawerTransitionListener {
+            backgroundAnimationProgress = it
+        }
+        drawerContainer.setTransitionListener(ringerDrawerTransitionListener)
+        volumeDialogBackgroundView.background = volumeDialogBackgroundView.background.mutate()
         viewModel.ringerViewModel
             .onEach { ringerState ->
                 when (ringerState) {
@@ -87,10 +110,8 @@
                                     selectedButtonUiModel,
                                     unselectedButtonUiModel,
                                 )
+                                ringerDrawerTransitionListener.setProgressChangeEnabled(true)
                                 drawerContainer.closeDrawer(uiModel.currentButtonIndex)
-                                volumeDialogBackgroundView.setBackgroundResource(
-                                    R.drawable.volume_dialog_background
-                                )
                             }
 
                             is RingerDrawerState.Closed -> {
@@ -103,11 +124,31 @@
                                         uiModel,
                                         selectedButtonUiModel,
                                         unselectedButtonUiModel,
+                                        onProgressChanged = { progress, isReverse ->
+                                            // Let's make button progress when switching matches
+                                            // motionLayout transition progress. When full radius,
+                                            // progress is 0.0. When small radius, progress is 1.0.
+                                            backgroundAnimationProgress =
+                                                if (isReverse) {
+                                                    1F - progress
+                                                } else {
+                                                    progress
+                                                }
+                                        },
                                     ) {
+                                        if (
+                                            uiModel.currentButtonIndex ==
+                                                uiModel.availableButtons.size - 1
+                                        ) {
+                                            ringerDrawerTransitionListener.setProgressChangeEnabled(
+                                                false
+                                            )
+                                        } else {
+                                            ringerDrawerTransitionListener.setProgressChangeEnabled(
+                                                true
+                                            )
+                                        }
                                         drawerContainer.closeDrawer(uiModel.currentButtonIndex)
-                                        volumeDialogBackgroundView.setBackgroundResource(
-                                            R.drawable.volume_dialog_background
-                                        )
                                     }
                                 }
                             }
@@ -120,16 +161,18 @@
                                     unselectedButtonUiModel,
                                 )
                                 // Open drawer
+                                if (
+                                    uiModel.currentButtonIndex == uiModel.availableButtons.size - 1
+                                ) {
+                                    ringerDrawerTransitionListener.setProgressChangeEnabled(false)
+                                } else {
+                                    ringerDrawerTransitionListener.setProgressChangeEnabled(true)
+                                }
                                 drawerContainer.transitionToState(
                                     R.id.volume_dialog_ringer_drawer_open
                                 )
-                                if (
-                                    uiModel.currentButtonIndex != uiModel.availableButtons.size - 1
-                                ) {
-                                    volumeDialogBackgroundView.setBackgroundResource(
-                                        R.drawable.volume_dialog_background_small_radius
-                                    )
-                                }
+                                volumeDialogBackgroundView.background =
+                                    volumeDialogBackgroundView.background.mutate()
                             }
                         }
                     }
@@ -150,6 +193,7 @@
         uiModel: RingerViewModel,
         selectedButtonUiModel: RingerButtonUiModel,
         unselectedButtonUiModel: RingerButtonUiModel,
+        onProgressChanged: (Float, Boolean) -> Unit = { _, _ -> },
         onAnimationEnd: Runnable? = null,
     ) {
         ensureChildCount(R.layout.volume_ringer_button, uiModel.availableButtons.size)
@@ -177,10 +221,26 @@
                         CLOSE_DRAWER_DELAY,
                     )
                 }
-
-            // We only need to execute on roundness animation end once.
-            selectedButton.animateTo(selectedButtonUiModel, roundnessAnimationEndListener)
-            unselectedButton.animateTo(unselectedButtonUiModel)
+            // We only need to execute on roundness animation end and volume dialog background
+            // progress update once because these changes should be applied once on volume dialog
+            // background and ringer drawer views.
+            selectedButton.animateTo(
+                selectedButtonUiModel,
+                if (uiModel.currentButtonIndex == count - 1) {
+                    onProgressChanged
+                } else {
+                    { _, _ -> }
+                },
+                roundnessAnimationEndListener,
+            )
+            unselectedButton.animateTo(
+                unselectedButtonUiModel,
+                if (previousIndex == count - 1) {
+                    onProgressChanged
+                } else {
+                    { _, _ -> }
+                },
+            )
         } else {
             bindButtons(viewModel, uiModel, onAnimationEnd)
         }
@@ -366,6 +426,7 @@
 
     private suspend fun ImageButton.animateTo(
         ringerButtonUiModel: RingerButtonUiModel,
+        onProgressChanged: (Float, Boolean) -> Unit = { _, _ -> },
         roundnessAnimationEndListener: DynamicAnimation.OnAnimationEndListener? = null,
     ) {
         val roundnessAnimation =
@@ -376,6 +437,7 @@
             ringerButtonUiModel.cornerRadius - (background as GradientDrawable).cornerRadius
         val roundnessAnimationUpdateListener =
             DynamicAnimation.OnAnimationUpdateListener { _, value, _ ->
+                onProgressChanged(value, cornerRadiusDiff > 0F)
                 (background as GradientDrawable).cornerRadius = radius + value * cornerRadiusDiff
                 background.invalidateSelf()
             }
@@ -406,4 +468,9 @@
             )
         }
     }
+
+    private fun View.applyCorners(fullRadius: Int, diff: Int, progress: Float) {
+        (background as GradientDrawable).cornerRadius = fullRadius - progress * diff
+        background.invalidateSelf()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/util/VolumeDialogRingerDrawerTransitionListener.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/util/VolumeDialogRingerDrawerTransitionListener.kt
new file mode 100644
index 0000000..6e3db0a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/util/VolumeDialogRingerDrawerTransitionListener.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.volume.dialog.ringer.ui.util
+
+import androidx.constraintlayout.motion.widget.MotionLayout
+
+class VolumeDialogRingerDrawerTransitionListener(private val onProgressChanged: (Float) -> Unit) :
+    MotionLayout.TransitionListener {
+
+    private var notifyProgressChangeEnabled = true
+
+    fun setProgressChangeEnabled(enabled: Boolean) {
+        notifyProgressChangeEnabled = enabled
+    }
+
+    override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) {}
+
+    override fun onTransitionChange(
+        motionLayout: MotionLayout?,
+        startId: Int,
+        endId: Int,
+        progress: Float,
+    ) {
+        if (notifyProgressChangeEnabled) {
+            onProgressChanged(progress)
+        }
+    }
+
+    override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {}
+
+    override fun onTransitionTrigger(
+        motionLayout: MotionLayout?,
+        triggerId: Int,
+        positive: Boolean,
+        progress: Float,
+    ) {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
index e646636..627d75e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
@@ -21,10 +21,13 @@
 import android.media.AudioManager.RINGER_MODE_NORMAL
 import android.media.AudioManager.RINGER_MODE_SILENT
 import android.media.AudioManager.RINGER_MODE_VIBRATE
+import android.media.AudioManager.STREAM_RING
 import android.os.VibrationEffect
 import android.widget.Toast
 import com.android.internal.R as internalR
 import com.android.settingslib.Utils
+import com.android.settingslib.notification.domain.interactor.NotificationsSoundPolicyInteractor
+import com.android.settingslib.volume.shared.model.AudioStream
 import com.android.settingslib.volume.shared.model.RingerMode
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
@@ -57,7 +60,8 @@
     @Application private val applicationContext: Context,
     @VolumeDialog private val coroutineScope: CoroutineScope,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
-    private val interactor: VolumeDialogRingerInteractor,
+    soundPolicyInteractor: NotificationsSoundPolicyInteractor,
+    private val ringerInteractor: VolumeDialogRingerInteractor,
     private val vibrator: VibratorHelper,
     private val volumeDialogLogger: VolumeDialogLogger,
     private val visibilityInteractor: VolumeDialogVisibilityInteractor,
@@ -66,10 +70,14 @@
     private val drawerState = MutableStateFlow<RingerDrawerState>(RingerDrawerState.Initial)
 
     val ringerViewModel: StateFlow<RingerViewModelState> =
-        combine(interactor.ringerModel, drawerState) { ringerModel, state ->
+        combine(
+                soundPolicyInteractor.isZenMuted(AudioStream(STREAM_RING)),
+                ringerInteractor.ringerModel,
+                drawerState,
+            ) { isZenMuted, ringerModel, state ->
                 level = ringerModel.level
                 levelMax = ringerModel.levelMax
-                ringerModel.toViewModel(state)
+                ringerModel.toViewModel(state, isZenMuted)
             }
             .flowOn(backgroundDispatcher)
             .stateIn(coroutineScope, SharingStarted.Eagerly, RingerViewModelState.Unavailable)
@@ -90,7 +98,7 @@
             Events.writeEvent(Events.EVENT_RINGER_TOGGLE, ringerMode.value)
             provideTouchFeedback(ringerMode)
             maybeShowToast(ringerMode)
-            interactor.setRingerMode(ringerMode)
+            ringerInteractor.setRingerMode(ringerMode)
         }
         visibilityInteractor.resetDismissTimeout()
         drawerState.value =
@@ -113,7 +121,7 @@
     private fun provideTouchFeedback(ringerMode: RingerMode) {
         when (ringerMode.value) {
             RINGER_MODE_NORMAL -> {
-                interactor.scheduleTouchFeedback()
+                ringerInteractor.scheduleTouchFeedback()
                 null
             }
             RINGER_MODE_SILENT -> VibrationEffect.get(VibrationEffect.EFFECT_CLICK)
@@ -123,7 +131,8 @@
     }
 
     private fun VolumeDialogRingerModel.toViewModel(
-        drawerState: RingerDrawerState
+        drawerState: RingerDrawerState,
+        isZenMuted: Boolean,
     ): RingerViewModelState {
         val currentIndex = availableModes.indexOf(currentRingerMode)
         if (currentIndex == -1) {
@@ -132,10 +141,11 @@
         return if (currentIndex == -1 || isSingleVolume) {
             RingerViewModelState.Unavailable
         } else {
-            toButtonViewModel(currentRingerMode, isSelectedButton = true)?.let {
+            toButtonViewModel(currentRingerMode, isZenMuted, isSelectedButton = true)?.let {
                 RingerViewModelState.Available(
                     RingerViewModel(
-                        availableButtons = availableModes.map { mode -> toButtonViewModel(mode) },
+                        availableButtons =
+                            availableModes.map { mode -> toButtonViewModel(mode, isZenMuted) },
                         currentButtonIndex = currentIndex,
                         selectedButton = it,
                         drawerState = drawerState,
@@ -147,6 +157,7 @@
 
     private fun VolumeDialogRingerModel.toButtonViewModel(
         ringerMode: RingerMode,
+        isZenMuted: Boolean,
         isSelectedButton: Boolean = false,
     ): RingerButtonViewModel? {
         return when (ringerMode.value) {
@@ -176,7 +187,7 @@
                 )
             RINGER_MODE_NORMAL ->
                 when {
-                    isMuted && isEnabled ->
+                    isMuted && !isZenMuted ->
                         RingerButtonViewModel(
                             imageResId =
                                 if (isSelectedButton) {
@@ -226,7 +237,7 @@
 
     private fun maybeShowToast(ringerMode: RingerMode) {
         coroutineScope.launch {
-            val seenToastCount = interactor.getToastCount()
+            val seenToastCount = ringerInteractor.getToastCount()
             if (seenToastCount > SHOW_RINGER_TOAST_COUNT) {
                 return@launch
             }
@@ -260,7 +271,7 @@
                         )
                 }
             toastText?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_SHORT).show() }
-            interactor.updateToastCount(seenToastCount)
+            ringerInteractor.updateToastCount(seenToastCount)
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
index e52bad9..f305246 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
@@ -18,11 +18,12 @@
 
 import android.animation.Animator
 import android.animation.ObjectAnimator
+import android.annotation.SuppressLint
 import android.view.View
 import android.view.animation.DecelerateInterpolator
 import com.android.systemui.res.R
-import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel
 import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderStateModel
 import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel
 import com.android.systemui.volume.dialog.ui.utils.JankListenerFactory
 import com.android.systemui.volume.dialog.ui.utils.awaitAnimation
@@ -48,24 +49,27 @@
         val sliderView: Slider =
             view.requireViewById<Slider>(R.id.volume_dialog_slider).apply {
                 labelBehavior = LabelFormatter.LABEL_GONE
+                trackIconActiveColor = trackInactiveTintList
             }
         sliderView.addOnChangeListener { _, value, fromUser ->
             viewModel.setStreamVolume(value.roundToInt(), fromUser)
         }
 
-        viewModel.model.onEach { it.bindToSlider(sliderView) }.launchIn(this)
+        viewModel.state.onEach { it.bindToSlider(sliderView) }.launchIn(this)
     }
 
-    private suspend fun VolumeDialogStreamModel.bindToSlider(slider: Slider) {
+    @SuppressLint("UseCompatLoadingForDrawables")
+    private suspend fun VolumeDialogSliderStateModel.bindToSlider(slider: Slider) {
         with(slider) {
-            valueFrom = levelMin.toFloat()
-            valueTo = levelMax.toFloat()
+            valueFrom = minValue
+            valueTo = maxValue
             // coerce the current value to the new value range before animating it
             value = value.coerceIn(valueFrom, valueTo)
             setValueAnimated(
-                level.toFloat(),
+                value,
                 jankListenerFactory.update(this, PROGRESS_CHANGE_ANIMATION_DURATION_MS),
             )
+            trackIconActiveEnd = context.getDrawable(iconRes)
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt
new file mode 100644
index 0000000..5c39b6f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.volume.dialog.sliders.ui.viewmodel
+
+import android.media.AudioManager
+import androidx.annotation.DrawableRes
+import com.android.settingslib.notification.domain.interactor.NotificationsSoundPolicyInteractor
+import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor
+import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.settingslib.volume.shared.model.RingerMode
+import com.android.systemui.res.R
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
+
+class VolumeDialogSliderIconProvider
+@Inject
+constructor(
+    private val notificationsSoundPolicyInteractor: NotificationsSoundPolicyInteractor,
+    private val audioVolumeInteractor: AudioVolumeInteractor,
+) {
+
+    @DrawableRes
+    fun getStreamIcon(
+        stream: Int,
+        level: Int,
+        levelMin: Int,
+        levelMax: Int,
+        isMuted: Boolean,
+        isRoutedToBluetooth: Boolean,
+    ): Flow<Int> {
+        return combine(
+            notificationsSoundPolicyInteractor.isZenMuted(AudioStream(stream)),
+            ringerModeForStream(stream),
+        ) { isZenMuted, ringerMode ->
+            val isStreamOffline = level == 0 || isMuted
+            if (isZenMuted) {
+                // TODO(b/372466264) use icon for the corresponding zenmode
+                return@combine com.android.internal.R.drawable.ic_qs_dnd
+            }
+            when (ringerMode?.value) {
+                AudioManager.RINGER_MODE_VIBRATE ->
+                    return@combine R.drawable.ic_volume_ringer_vibrate
+                AudioManager.RINGER_MODE_SILENT -> return@combine R.drawable.ic_ring_volume_off
+            }
+            if (isRoutedToBluetooth) {
+                return@combine if (stream == AudioManager.STREAM_VOICE_CALL) {
+                    R.drawable.ic_volume_bt_sco
+                } else {
+                    if (isStreamOffline) {
+                        R.drawable.ic_volume_media_bt_mute
+                    } else {
+                        R.drawable.ic_volume_media_bt
+                    }
+                }
+            }
+
+            return@combine if (isStreamOffline) {
+                getMutedIconForStream(stream) ?: getIconForStream(stream)
+            } else {
+                if (level < (levelMax + levelMin) / 2) {
+                    // This icon is different on TV
+                    R.drawable.ic_volume_media_low
+                } else {
+                    getIconForStream(stream)
+                }
+            }
+        }
+    }
+
+    @DrawableRes
+    private fun getMutedIconForStream(stream: Int): Int? {
+        return when (stream) {
+            AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_media_mute
+            AudioManager.STREAM_NOTIFICATION -> R.drawable.ic_volume_ringer_mute
+            AudioManager.STREAM_ALARM -> R.drawable.ic_volume_alarm_mute
+            AudioManager.STREAM_SYSTEM -> R.drawable.ic_volume_system_mute
+            else -> null
+        }
+    }
+
+    @DrawableRes
+    private fun getIconForStream(stream: Int): Int {
+        return when (stream) {
+            AudioManager.STREAM_ACCESSIBILITY -> R.drawable.ic_volume_accessibility
+            AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_media
+            AudioManager.STREAM_RING -> R.drawable.ic_ring_volume
+            AudioManager.STREAM_NOTIFICATION -> R.drawable.ic_volume_ringer
+            AudioManager.STREAM_ALARM -> R.drawable.ic_alarm
+            AudioManager.STREAM_VOICE_CALL -> com.android.internal.R.drawable.ic_phone
+            AudioManager.STREAM_SYSTEM -> R.drawable.ic_volume_system
+            else -> error("Unsupported stream: $stream")
+        }
+    }
+
+    /**
+     * Emits [RingerMode] for the [stream] if it's affecting it and null when [RingerMode] doesn't
+     * affect the [stream]
+     */
+    private fun ringerModeForStream(stream: Int): Flow<RingerMode?> {
+        return if (stream == AudioManager.STREAM_RING) {
+            audioVolumeInteractor.ringerMode
+        } else {
+            flowOf(null)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
new file mode 100644
index 0000000..5750c04
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.volume.dialog.sliders.ui.viewmodel
+
+import androidx.annotation.DrawableRes
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel
+
+data class VolumeDialogSliderStateModel(
+    val minValue: Float,
+    val maxValue: Float,
+    val value: Float,
+    @DrawableRes val iconRes: Int,
+)
+
+fun VolumeDialogStreamModel.toStateModel(@DrawableRes iconRes: Int): VolumeDialogSliderStateModel {
+    return VolumeDialogSliderStateModel(
+        minValue = levelMin.toFloat(),
+        value = level.toFloat(),
+        maxValue = levelMax.toFloat(),
+        iconRes = iconRes,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
index 6dd5b63..2d56524 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
@@ -32,7 +32,9 @@
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.stateIn
 
@@ -56,12 +58,12 @@
     private val interactor: VolumeDialogSliderInteractor,
     private val visibilityInteractor: VolumeDialogVisibilityInteractor,
     @VolumeDialog private val coroutineScope: CoroutineScope,
+    private val volumeDialogSliderIconProvider: VolumeDialogSliderIconProvider,
     private val systemClock: SystemClock,
 ) {
 
     private val userVolumeUpdates = MutableStateFlow<VolumeUpdate?>(null)
-
-    val model: Flow<VolumeDialogStreamModel> =
+    private val model: Flow<VolumeDialogStreamModel> =
         interactor.slider
             .filter {
                 val lastVolumeUpdateTime = userVolumeUpdates.value?.timestampMillis ?: 0
@@ -70,6 +72,21 @@
             .stateIn(coroutineScope, SharingStarted.Eagerly, null)
             .filterNotNull()
 
+    val state: Flow<VolumeDialogSliderStateModel> =
+        model.flatMapLatest { streamModel ->
+            with(streamModel) {
+                    volumeDialogSliderIconProvider.getStreamIcon(
+                        stream = stream,
+                        level = level,
+                        levelMin = levelMin,
+                        levelMax = levelMax,
+                        isMuted = muted,
+                        isRoutedToBluetooth = routedToBluetooth,
+                    )
+                }
+                .map { icon -> streamModel.toStateModel(icon) }
+        }
+
     init {
         userVolumeUpdates
             .filterNotNull()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
new file mode 100644
index 0000000..d7fcb6a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
@@ -0,0 +1,471 @@
+/*
+ * Copyright 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.education.domain.interactor
+
+import android.content.pm.UserInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.contextualeducation.GestureType
+import com.android.systemui.contextualeducation.GestureType.ALL_APPS
+import com.android.systemui.contextualeducation.GestureType.BACK
+import com.android.systemui.contextualeducation.GestureType.HOME
+import com.android.systemui.contextualeducation.GestureType.OVERVIEW
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.education.data.model.GestureEduModel
+import com.android.systemui.education.data.repository.contextualEducationRepository
+import com.android.systemui.education.data.repository.fakeEduClock
+import com.android.systemui.education.shared.model.EducationUiType
+import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType
+import com.android.systemui.inputdevice.tutorial.tutorialSchedulerRepository
+import com.android.systemui.keyboard.data.repository.keyboardRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
+import com.android.systemui.testKosmos
+import com.android.systemui.touchpad.data.repository.touchpadRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.verify
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+@kotlinx.coroutines.ExperimentalCoroutinesApi
+class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: GestureType) :
+    SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val contextualEduInteractor = kosmos.contextualEducationInteractor
+    private val repository = kosmos.contextualEducationRepository
+    private val touchpadRepository = kosmos.touchpadRepository
+    private val keyboardRepository = kosmos.keyboardRepository
+    private val tutorialSchedulerRepository = kosmos.tutorialSchedulerRepository
+    private val userRepository = kosmos.fakeUserRepository
+    private val overviewProxyService = kosmos.mockOverviewProxyService
+
+    private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor
+    private val eduClock = kosmos.fakeEduClock
+    private val minDurationForNextEdu =
+        KeyboardTouchpadEduInteractor.minIntervalBetweenEdu + 1.seconds
+    private val initialDelayElapsedDuration =
+        KeyboardTouchpadEduInteractor.initialDelayDuration + 1.seconds
+
+    @Before
+    fun setup() {
+        underTest.start()
+        contextualEduInteractor.start()
+        userRepository.setUserInfos(USER_INFOS)
+        testScope.launch {
+            contextualEduInteractor.updateKeyboardFirstConnectionTime()
+            contextualEduInteractor.updateTouchpadFirstConnectionTime()
+        }
+    }
+
+    @Test
+    fun newEducationInfoOnMaxSignalCountReached() =
+        testScope.runTest {
+            triggerMaxEducationSignals(gestureType)
+            val model by collectLastValue(underTest.educationTriggered)
+
+            assertThat(model?.gestureType).isEqualTo(gestureType)
+        }
+
+    @Test
+    fun newEducationToastOn1stEducation() =
+        testScope.runTest {
+            val model by collectLastValue(underTest.educationTriggered)
+            triggerMaxEducationSignals(gestureType)
+
+            assertThat(model?.educationUiType).isEqualTo(EducationUiType.Toast)
+        }
+
+    @Test
+    fun newEducationNotificationOn2ndEducation() =
+        testScope.runTest {
+            val model by collectLastValue(underTest.educationTriggered)
+            triggerMaxEducationSignals(gestureType)
+            // runCurrent() to trigger 1st education
+            runCurrent()
+
+            eduClock.offset(minDurationForNextEdu)
+            triggerMaxEducationSignals(gestureType)
+
+            assertThat(model?.educationUiType).isEqualTo(EducationUiType.Notification)
+        }
+
+    @Test
+    fun noEducationInfoBeforeMaxSignalCountReached() =
+        testScope.runTest {
+            contextualEduInteractor.incrementSignalCount(gestureType)
+            val model by collectLastValue(underTest.educationTriggered)
+            assertThat(model).isNull()
+        }
+
+    @Test
+    fun noEducationInfoWhenShortcutTriggeredPreviously() =
+        testScope.runTest {
+            val model by collectLastValue(underTest.educationTriggered)
+            contextualEduInteractor.updateShortcutTriggerTime(gestureType)
+            triggerMaxEducationSignals(gestureType)
+            assertThat(model).isNull()
+        }
+
+    @Test
+    fun no2ndEducationBeforeMinEduIntervalReached() =
+        testScope.runTest {
+            val models by collectValues(underTest.educationTriggered)
+            triggerMaxEducationSignals(gestureType)
+            runCurrent()
+
+            // Offset a duration that is less than the required education interval
+            eduClock.offset(1.seconds)
+            triggerMaxEducationSignals(gestureType)
+            runCurrent()
+
+            assertThat(models.filterNotNull().size).isEqualTo(1)
+        }
+
+    @Test
+    fun noNewEducationInfoAfterMaxEducationCountReached() =
+        testScope.runTest {
+            val models by collectValues(underTest.educationTriggered)
+            // Trigger 2 educations
+            triggerMaxEducationSignals(gestureType)
+            runCurrent()
+            eduClock.offset(minDurationForNextEdu)
+            triggerMaxEducationSignals(gestureType)
+            runCurrent()
+
+            // Try triggering 3rd education
+            eduClock.offset(minDurationForNextEdu)
+            triggerMaxEducationSignals(gestureType)
+
+            assertThat(models.filterNotNull().size).isEqualTo(2)
+        }
+
+    @Test
+    fun startNewUsageSessionWhen2ndSignalReceivedAfterSessionDeadline() =
+        testScope.runTest {
+            val model by
+                collectLastValue(
+                    kosmos.contextualEducationRepository.readGestureEduModelFlow(gestureType)
+                )
+            contextualEduInteractor.incrementSignalCount(gestureType)
+            eduClock.offset(KeyboardTouchpadEduInteractor.usageSessionDuration.plus(1.seconds))
+            val secondSignalReceivedTime = eduClock.instant()
+            contextualEduInteractor.incrementSignalCount(gestureType)
+
+            assertThat(model)
+                .isEqualTo(
+                    GestureEduModel(
+                        signalCount = 1,
+                        usageSessionStartTime = secondSignalReceivedTime,
+                        userId = 0,
+                        gestureType = gestureType,
+                    )
+                )
+        }
+
+    @Test
+    fun newTouchpadConnectionTimeOnFirstTouchpadConnected() =
+        testScope.runTest {
+            setIsAnyTouchpadConnected(true)
+            val model = contextualEduInteractor.getEduDeviceConnectionTime()
+            assertThat(model.touchpadFirstConnectionTime).isEqualTo(eduClock.instant())
+        }
+
+    @Test
+    fun unchangedTouchpadConnectionTimeOnSecondConnection() =
+        testScope.runTest {
+            val firstConnectionTime = eduClock.instant()
+            setIsAnyTouchpadConnected(true)
+            setIsAnyTouchpadConnected(false)
+
+            eduClock.offset(1.hours)
+            setIsAnyTouchpadConnected(true)
+
+            val model = contextualEduInteractor.getEduDeviceConnectionTime()
+            assertThat(model.touchpadFirstConnectionTime).isEqualTo(firstConnectionTime)
+        }
+
+    @Test
+    fun newTouchpadConnectionTimeOnUserChanged() =
+        testScope.runTest {
+            // Touchpad connected for user 0
+            setIsAnyTouchpadConnected(true)
+
+            // Change user
+            eduClock.offset(1.hours)
+            val newUserFirstConnectionTime = eduClock.instant()
+            userRepository.setSelectedUserInfo(USER_INFOS[0])
+            runCurrent()
+
+            val model = contextualEduInteractor.getEduDeviceConnectionTime()
+            assertThat(model.touchpadFirstConnectionTime).isEqualTo(newUserFirstConnectionTime)
+        }
+
+    @Test
+    fun newKeyboardConnectionTimeOnKeyboardConnected() =
+        testScope.runTest {
+            setIsAnyKeyboardConnected(true)
+            val model = contextualEduInteractor.getEduDeviceConnectionTime()
+            assertThat(model.keyboardFirstConnectionTime).isEqualTo(eduClock.instant())
+        }
+
+    @Test
+    fun unchangedKeyboardConnectionTimeOnSecondConnection() =
+        testScope.runTest {
+            val firstConnectionTime = eduClock.instant()
+            setIsAnyKeyboardConnected(true)
+            setIsAnyKeyboardConnected(false)
+
+            eduClock.offset(1.hours)
+            setIsAnyKeyboardConnected(true)
+
+            val model = contextualEduInteractor.getEduDeviceConnectionTime()
+            assertThat(model.keyboardFirstConnectionTime).isEqualTo(firstConnectionTime)
+        }
+
+    @Test
+    fun newKeyboardConnectionTimeOnUserChanged() =
+        testScope.runTest {
+            // Keyboard connected for user 0
+            setIsAnyKeyboardConnected(true)
+
+            // Change user
+            eduClock.offset(1.hours)
+            val newUserFirstConnectionTime = eduClock.instant()
+            userRepository.setSelectedUserInfo(USER_INFOS[0])
+            runCurrent()
+
+            val model = contextualEduInteractor.getEduDeviceConnectionTime()
+            assertThat(model.keyboardFirstConnectionTime).isEqualTo(newUserFirstConnectionTime)
+        }
+
+    @Test
+    fun updateShortcutTimeOnKeyboardShortcutTriggered() =
+        testScope.runTest {
+            // Only All Apps needs to update the keyboard shortcut
+            assumeTrue(gestureType == ALL_APPS)
+            kosmos.contextualEducationRepository.setKeyboardShortcutTriggered(ALL_APPS)
+
+            val model by
+                collectLastValue(
+                    kosmos.contextualEducationRepository.readGestureEduModelFlow(ALL_APPS)
+                )
+            assertThat(model?.lastShortcutTriggeredTime).isEqualTo(eduClock.instant())
+        }
+
+    @Test
+    fun dataUpdatedOnIncrementSignalCountWhenTouchpadConnected() =
+        testScope.runTest {
+            assumeTrue(gestureType != ALL_APPS)
+            setUpForInitialDelayElapse()
+            touchpadRepository.setIsAnyTouchpadConnected(true)
+
+            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+            val originalValue = model!!.signalCount
+            val listener = getOverviewProxyListener()
+            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+        }
+
+    @Test
+    fun dataUnchangedOnIncrementSignalCountWhenTouchpadDisconnected() =
+        testScope.runTest {
+            setUpForInitialDelayElapse()
+            touchpadRepository.setIsAnyTouchpadConnected(false)
+
+            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+            val originalValue = model!!.signalCount
+            val listener = getOverviewProxyListener()
+            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+            assertThat(model?.signalCount).isEqualTo(originalValue)
+        }
+
+    @Test
+    fun dataUpdatedOnIncrementSignalCountWhenKeyboardConnected() =
+        testScope.runTest {
+            assumeTrue(gestureType == ALL_APPS)
+            setUpForInitialDelayElapse()
+            keyboardRepository.setIsAnyKeyboardConnected(true)
+
+            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+            val originalValue = model!!.signalCount
+            val listener = getOverviewProxyListener()
+            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+        }
+
+    @Test
+    fun dataUnchangedOnIncrementSignalCountWhenKeyboardDisconnected() =
+        testScope.runTest {
+            setUpForInitialDelayElapse()
+            keyboardRepository.setIsAnyKeyboardConnected(false)
+
+            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+            val originalValue = model!!.signalCount
+            val listener = getOverviewProxyListener()
+            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+            assertThat(model?.signalCount).isEqualTo(originalValue)
+        }
+
+    @Test
+    fun dataAddedOnUpdateShortcutTriggerTime() =
+        testScope.runTest {
+            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+            assertThat(model?.lastShortcutTriggeredTime).isNull()
+
+            val listener = getOverviewProxyListener()
+            listener.updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType)
+
+            assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant())
+        }
+
+    @Test
+    fun dataUpdatedOnIncrementSignalCountAfterInitialDelay() =
+        testScope.runTest {
+            setUpForDeviceConnection()
+            tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, eduClock.instant())
+
+            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+            val originalValue = model!!.signalCount
+            eduClock.offset(initialDelayElapsedDuration)
+            val listener = getOverviewProxyListener()
+            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+        }
+
+    @Test
+    fun dataUnchangedOnIncrementSignalCountBeforeInitialDelay() =
+        testScope.runTest {
+            setUpForDeviceConnection()
+            tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, eduClock.instant())
+
+            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+            val originalValue = model!!.signalCount
+            // No offset to the clock to simulate update before initial delay
+            val listener = getOverviewProxyListener()
+            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+            assertThat(model?.signalCount).isEqualTo(originalValue)
+        }
+
+    @Test
+    fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchTime() =
+        testScope.runTest {
+            // No update to OOBE launch time to simulate no OOBE is launched yet
+            setUpForDeviceConnection()
+
+            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+            val originalValue = model!!.signalCount
+            val listener = getOverviewProxyListener()
+            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+            assertThat(model?.signalCount).isEqualTo(originalValue)
+        }
+
+    private suspend fun setUpForInitialDelayElapse() {
+        tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, eduClock.instant())
+        tutorialSchedulerRepository.updateLaunchTime(DeviceType.KEYBOARD, eduClock.instant())
+        eduClock.offset(initialDelayElapsedDuration)
+    }
+
+    fun logMetricsForToastEducation() =
+        testScope.runTest {
+            triggerMaxEducationSignals(gestureType)
+            runCurrent()
+
+            verify(kosmos.mockEduMetricsLogger)
+                .logContextualEducationTriggered(gestureType, EducationUiType.Toast)
+        }
+
+    @Test
+    fun logMetricsForNotificationEducation() =
+        testScope.runTest {
+            triggerMaxEducationSignals(gestureType)
+            runCurrent()
+
+            eduClock.offset(minDurationForNextEdu)
+            triggerMaxEducationSignals(gestureType)
+            runCurrent()
+
+            verify(kosmos.mockEduMetricsLogger)
+                .logContextualEducationTriggered(gestureType, EducationUiType.Notification)
+        }
+
+    @After
+    fun clear() {
+        testScope.launch { tutorialSchedulerRepository.clear() }
+    }
+
+    private suspend fun triggerMaxEducationSignals(gestureType: GestureType) {
+        // Increment max number of signal to try triggering education
+        for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) {
+            contextualEduInteractor.incrementSignalCount(gestureType)
+        }
+    }
+
+    private fun TestScope.setIsAnyTouchpadConnected(isConnected: Boolean) {
+        touchpadRepository.setIsAnyTouchpadConnected(isConnected)
+        runCurrent()
+    }
+
+    private fun TestScope.setIsAnyKeyboardConnected(isConnected: Boolean) {
+        keyboardRepository.setIsAnyKeyboardConnected(isConnected)
+        runCurrent()
+    }
+
+    private fun setUpForDeviceConnection() {
+        touchpadRepository.setIsAnyTouchpadConnected(true)
+        keyboardRepository.setIsAnyKeyboardConnected(true)
+    }
+
+    private fun getOverviewProxyListener(): OverviewProxyListener {
+        val listenerCaptor = argumentCaptor<OverviewProxyListener>()
+        verify(overviewProxyService).addCallback(listenerCaptor.capture())
+        return listenerCaptor.firstValue
+    }
+
+    companion object {
+        private val USER_INFOS = listOf(UserInfo(101, "Second User", 0))
+
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getGestureTypes(): List<GestureType> {
+            return listOf(BACK, HOME, OVERVIEW, ALL_APPS)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
index 2a6d29c..580f631 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2024 The Android Open Source Project
+ * 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.
@@ -16,19 +16,17 @@
 
 package com.android.systemui.education.domain.interactor
 
-import android.content.pm.UserInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.contextualeducation.GestureType
-import com.android.systemui.contextualeducation.GestureType.ALL_APPS
 import com.android.systemui.contextualeducation.GestureType.BACK
 import com.android.systemui.contextualeducation.GestureType.HOME
 import com.android.systemui.contextualeducation.GestureType.OVERVIEW
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
-import com.android.systemui.education.data.model.GestureEduModel
-import com.android.systemui.education.data.repository.contextualEducationRepository
 import com.android.systemui.education.data.repository.fakeEduClock
+import com.android.systemui.education.shared.model.EducationInfo
 import com.android.systemui.education.shared.model.EducationUiType
 import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType
 import com.android.systemui.inputdevice.tutorial.tutorialSchedulerRepository
@@ -37,50 +35,42 @@
 import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
 import com.android.systemui.testKosmos
 import com.android.systemui.touchpad.data.repository.touchpadRepository
-import com.android.systemui.user.data.repository.fakeUserRepository
 import com.google.common.truth.Truth.assertThat
-import kotlin.time.Duration.Companion.hours
 import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
-import org.junit.After
-import org.junit.Assume.assumeTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.verify
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4
-import platform.test.runner.parameterized.Parameters
 
 @SmallTest
-@RunWith(ParameterizedAndroidJunit4::class)
+@RunWith(AndroidJUnit4::class)
 @kotlinx.coroutines.ExperimentalCoroutinesApi
-class KeyboardTouchpadEduInteractorTest(private val gestureType: GestureType) : SysuiTestCase() {
+class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val contextualEduInteractor = kosmos.contextualEducationInteractor
-    private val repository = kosmos.contextualEducationRepository
     private val touchpadRepository = kosmos.touchpadRepository
     private val keyboardRepository = kosmos.keyboardRepository
     private val tutorialSchedulerRepository = kosmos.tutorialSchedulerRepository
-    private val userRepository = kosmos.fakeUserRepository
     private val overviewProxyService = kosmos.mockOverviewProxyService
 
     private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor
     private val eduClock = kosmos.fakeEduClock
-    private val minDurationForNextEdu =
-        KeyboardTouchpadEduInteractor.minIntervalBetweenEdu + 1.seconds
     private val initialDelayElapsedDuration =
         KeyboardTouchpadEduInteractor.initialDelayDuration + 1.seconds
+    private val minIntervalForEduNotification =
+        KeyboardTouchpadEduInteractor.minIntervalBetweenEdu + 1.seconds
 
     @Before
     fun setup() {
         underTest.start()
         contextualEduInteractor.start()
-        userRepository.setUserInfos(USER_INFOS)
         testScope.launch {
             contextualEduInteractor.updateKeyboardFirstConnectionTime()
             contextualEduInteractor.updateTouchpadFirstConnectionTime()
@@ -88,312 +78,76 @@
     }
 
     @Test
-    fun newEducationInfoOnMaxSignalCountReached() =
-        testScope.runTest {
-            triggerMaxEducationSignals(gestureType)
-            val model by collectLastValue(underTest.educationTriggered)
-
-            assertThat(model?.gestureType).isEqualTo(gestureType)
-        }
-
-    @Test
-    fun newEducationToastOn1stEducation() =
-        testScope.runTest {
-            val model by collectLastValue(underTest.educationTriggered)
-            triggerMaxEducationSignals(gestureType)
-
-            assertThat(model?.educationUiType).isEqualTo(EducationUiType.Toast)
-        }
-
-    @Test
-    fun newEducationNotificationOn2ndEducation() =
-        testScope.runTest {
-            val model by collectLastValue(underTest.educationTriggered)
-            triggerMaxEducationSignals(gestureType)
-            // runCurrent() to trigger 1st education
-            runCurrent()
-
-            eduClock.offset(minDurationForNextEdu)
-            triggerMaxEducationSignals(gestureType)
-
-            assertThat(model?.educationUiType).isEqualTo(EducationUiType.Notification)
-        }
-
-    @Test
-    fun noEducationInfoBeforeMaxSignalCountReached() =
-        testScope.runTest {
-            contextualEduInteractor.incrementSignalCount(gestureType)
-            val model by collectLastValue(underTest.educationTriggered)
-            assertThat(model).isNull()
-        }
-
-    @Test
-    fun noEducationInfoWhenShortcutTriggeredPreviously() =
-        testScope.runTest {
-            val model by collectLastValue(underTest.educationTriggered)
-            contextualEduInteractor.updateShortcutTriggerTime(gestureType)
-            triggerMaxEducationSignals(gestureType)
-            assertThat(model).isNull()
-        }
-
-    @Test
-    fun no2ndEducationBeforeMinEduIntervalReached() =
-        testScope.runTest {
-            val models by collectValues(underTest.educationTriggered)
-            triggerMaxEducationSignals(gestureType)
-            runCurrent()
-
-            // Offset a duration that is less than the required education interval
-            eduClock.offset(1.seconds)
-            triggerMaxEducationSignals(gestureType)
-            runCurrent()
-
-            assertThat(models.filterNotNull().size).isEqualTo(1)
-        }
-
-    @Test
-    fun noNewEducationInfoAfterMaxEducationCountReached() =
-        testScope.runTest {
-            val models by collectValues(underTest.educationTriggered)
-            // Trigger 2 educations
-            triggerMaxEducationSignals(gestureType)
-            runCurrent()
-            eduClock.offset(minDurationForNextEdu)
-            triggerMaxEducationSignals(gestureType)
-            runCurrent()
-
-            // Try triggering 3rd education
-            eduClock.offset(minDurationForNextEdu)
-            triggerMaxEducationSignals(gestureType)
-
-            assertThat(models.filterNotNull().size).isEqualTo(2)
-        }
-
-    @Test
-    fun startNewUsageSessionWhen2ndSignalReceivedAfterSessionDeadline() =
-        testScope.runTest {
-            val model by
-                collectLastValue(
-                    kosmos.contextualEducationRepository.readGestureEduModelFlow(gestureType)
-                )
-            contextualEduInteractor.incrementSignalCount(gestureType)
-            eduClock.offset(KeyboardTouchpadEduInteractor.usageSessionDuration.plus(1.seconds))
-            val secondSignalReceivedTime = eduClock.instant()
-            contextualEduInteractor.incrementSignalCount(gestureType)
-
-            assertThat(model)
-                .isEqualTo(
-                    GestureEduModel(
-                        signalCount = 1,
-                        usageSessionStartTime = secondSignalReceivedTime,
-                        userId = 0,
-                        gestureType = gestureType,
-                    )
-                )
-        }
-
-    @Test
-    fun newTouchpadConnectionTimeOnFirstTouchpadConnected() =
-        testScope.runTest {
-            setIsAnyTouchpadConnected(true)
-            val model = contextualEduInteractor.getEduDeviceConnectionTime()
-            assertThat(model.touchpadFirstConnectionTime).isEqualTo(eduClock.instant())
-        }
-
-    @Test
-    fun unchangedTouchpadConnectionTimeOnSecondConnection() =
-        testScope.runTest {
-            val firstConnectionTime = eduClock.instant()
-            setIsAnyTouchpadConnected(true)
-            setIsAnyTouchpadConnected(false)
-
-            eduClock.offset(1.hours)
-            setIsAnyTouchpadConnected(true)
-
-            val model = contextualEduInteractor.getEduDeviceConnectionTime()
-            assertThat(model.touchpadFirstConnectionTime).isEqualTo(firstConnectionTime)
-        }
-
-    @Test
-    fun newTouchpadConnectionTimeOnUserChanged() =
-        testScope.runTest {
-            // Touchpad connected for user 0
-            setIsAnyTouchpadConnected(true)
-
-            // Change user
-            eduClock.offset(1.hours)
-            val newUserFirstConnectionTime = eduClock.instant()
-            userRepository.setSelectedUserInfo(USER_INFOS[0])
-            runCurrent()
-
-            val model = contextualEduInteractor.getEduDeviceConnectionTime()
-            assertThat(model.touchpadFirstConnectionTime).isEqualTo(newUserFirstConnectionTime)
-        }
-
-    @Test
-    fun newKeyboardConnectionTimeOnKeyboardConnected() =
-        testScope.runTest {
-            setIsAnyKeyboardConnected(true)
-            val model = contextualEduInteractor.getEduDeviceConnectionTime()
-            assertThat(model.keyboardFirstConnectionTime).isEqualTo(eduClock.instant())
-        }
-
-    @Test
-    fun unchangedKeyboardConnectionTimeOnSecondConnection() =
-        testScope.runTest {
-            val firstConnectionTime = eduClock.instant()
-            setIsAnyKeyboardConnected(true)
-            setIsAnyKeyboardConnected(false)
-
-            eduClock.offset(1.hours)
-            setIsAnyKeyboardConnected(true)
-
-            val model = contextualEduInteractor.getEduDeviceConnectionTime()
-            assertThat(model.keyboardFirstConnectionTime).isEqualTo(firstConnectionTime)
-        }
-
-    @Test
-    fun newKeyboardConnectionTimeOnUserChanged() =
-        testScope.runTest {
-            // Keyboard connected for user 0
-            setIsAnyKeyboardConnected(true)
-
-            // Change user
-            eduClock.offset(1.hours)
-            val newUserFirstConnectionTime = eduClock.instant()
-            userRepository.setSelectedUserInfo(USER_INFOS[0])
-            runCurrent()
-
-            val model = contextualEduInteractor.getEduDeviceConnectionTime()
-            assertThat(model.keyboardFirstConnectionTime).isEqualTo(newUserFirstConnectionTime)
-        }
-
-    @Test
-    fun updateShortcutTimeOnKeyboardShortcutTriggered() =
-        testScope.runTest {
-            // Only All Apps needs to update the keyboard shortcut
-            assumeTrue(gestureType == ALL_APPS)
-            kosmos.contextualEducationRepository.setKeyboardShortcutTriggered(ALL_APPS)
-
-            val model by
-                collectLastValue(
-                    kosmos.contextualEducationRepository.readGestureEduModelFlow(ALL_APPS)
-                )
-            assertThat(model?.lastShortcutTriggeredTime).isEqualTo(eduClock.instant())
-        }
-
-    @Test
-    fun dataUpdatedOnIncrementSignalCountWhenTouchpadConnected() =
-        testScope.runTest {
-            assumeTrue(gestureType != ALL_APPS)
-            setUpForInitialDelayElapse()
-            touchpadRepository.setIsAnyTouchpadConnected(true)
-
-            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
-            val originalValue = model!!.signalCount
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
-
-            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
-        }
-
-    @Test
-    fun dataUnchangedOnIncrementSignalCountWhenTouchpadDisconnected() =
-        testScope.runTest {
-            setUpForInitialDelayElapse()
-            touchpadRepository.setIsAnyTouchpadConnected(false)
-
-            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
-            val originalValue = model!!.signalCount
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
-
-            assertThat(model?.signalCount).isEqualTo(originalValue)
-        }
-
-    @Test
-    fun dataUpdatedOnIncrementSignalCountWhenKeyboardConnected() =
-        testScope.runTest {
-            assumeTrue(gestureType == ALL_APPS)
-            setUpForInitialDelayElapse()
-            keyboardRepository.setIsAnyKeyboardConnected(true)
-
-            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
-            val originalValue = model!!.signalCount
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
-
-            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
-        }
-
-    @Test
-    fun dataUnchangedOnIncrementSignalCountWhenKeyboardDisconnected() =
-        testScope.runTest {
-            setUpForInitialDelayElapse()
-            keyboardRepository.setIsAnyKeyboardConnected(false)
-
-            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
-            val originalValue = model!!.signalCount
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
-
-            assertThat(model?.signalCount).isEqualTo(originalValue)
-        }
-
-    @Test
-    fun dataAddedOnUpdateShortcutTriggerTime() =
-        testScope.runTest {
-            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
-            assertThat(model?.lastShortcutTriggeredTime).isNull()
-
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType)
-
-            assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant())
-        }
-
-    @Test
-    fun dataUpdatedOnIncrementSignalCountAfterInitialDelay() =
+    fun newEducationToastBeforeMaxToastsPerSessionTriggered() =
         testScope.runTest {
             setUpForDeviceConnection()
-            tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, eduClock.instant())
+            setUpForInitialDelayElapse()
+            val model by collectLastValue(underTest.educationTriggered)
 
-            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
-            val originalValue = model!!.signalCount
-            eduClock.offset(initialDelayElapsedDuration)
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+            triggerEducation(HOME)
 
-            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+            assertThat(model).isEqualTo(EducationInfo(HOME, EducationUiType.Toast, userId = 0))
         }
 
     @Test
-    fun dataUnchangedOnIncrementSignalCountBeforeInitialDelay() =
+    fun noEducationToastAfterMaxToastsPerSessionTriggered() =
         testScope.runTest {
             setUpForDeviceConnection()
-            tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, eduClock.instant())
+            setUpForInitialDelayElapse()
+            val models by collectValues(underTest.educationTriggered.filterNotNull())
+            // Show two toasts of other gestures
+            triggerEducation(HOME)
+            triggerEducation(BACK)
 
-            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
-            val originalValue = model!!.signalCount
-            // No offset to the clock to simulate update before initial delay
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+            triggerEducation(OVERVIEW)
 
-            assertThat(model?.signalCount).isEqualTo(originalValue)
+            // No new toast education besides the 2 triggered at first
+            val firstEdu = EducationInfo(HOME, EducationUiType.Toast, userId = 0)
+            val secondEdu = EducationInfo(BACK, EducationUiType.Toast, userId = 0)
+            assertThat(models).containsExactly(firstEdu, secondEdu).inOrder()
         }
 
     @Test
-    fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchTime() =
+    fun newEducationToastAfterMinIntervalElapsedWhenMaxToastsPerSessionTriggered() =
         testScope.runTest {
-            // No update to OOBE launch time to simulate no OOBE is launched yet
             setUpForDeviceConnection()
+            setUpForInitialDelayElapse()
+            val models by collectValues(underTest.educationTriggered.filterNotNull())
+            // Show two toasts of other gestures
+            triggerEducation(HOME)
+            triggerEducation(BACK)
 
-            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
-            val originalValue = model!!.signalCount
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+            // Trigger toast after an usage session has elapsed
+            eduClock.offset(KeyboardTouchpadEduInteractor.usageSessionDuration + 1.seconds)
+            triggerEducation(OVERVIEW)
 
-            assertThat(model?.signalCount).isEqualTo(originalValue)
+            val firstEdu = EducationInfo(HOME, EducationUiType.Toast, userId = 0)
+            val secondEdu = EducationInfo(BACK, EducationUiType.Toast, userId = 0)
+            val thirdEdu = EducationInfo(OVERVIEW, EducationUiType.Toast, userId = 0)
+            assertThat(models).containsExactly(firstEdu, secondEdu, thirdEdu).inOrder()
+        }
+
+    @Test
+    fun newEducationNotificationAfterMaxToastsPerSessionTriggered() =
+        testScope.runTest {
+            setUpForDeviceConnection()
+            setUpForInitialDelayElapse()
+            val models by collectValues(underTest.educationTriggered.filterNotNull())
+            triggerEducation(BACK)
+
+            // Offset to let min interval for notification elapse so we could show edu notification
+            // for BACK. It would be a new usage session too because the interval (7 days) is
+            // longer than a usage session (3 days)
+            eduClock.offset(minIntervalForEduNotification)
+            triggerEducation(HOME)
+            triggerEducation(OVERVIEW)
+            triggerEducation(BACK)
+
+            val firstEdu = EducationInfo(BACK, EducationUiType.Toast, userId = 0)
+            val secondEdu = EducationInfo(HOME, EducationUiType.Toast, userId = 0)
+            val thirdEdu = EducationInfo(OVERVIEW, EducationUiType.Toast, userId = 0)
+            val fourthEdu = EducationInfo(BACK, EducationUiType.Notification, userId = 0)
+            assertThat(models).containsExactly(firstEdu, secondEdu, thirdEdu, fourthEdu).inOrder()
         }
 
     private suspend fun setUpForInitialDelayElapse() {
@@ -402,51 +156,6 @@
         eduClock.offset(initialDelayElapsedDuration)
     }
 
-    fun logMetricsForToastEducation() =
-        testScope.runTest {
-            triggerMaxEducationSignals(gestureType)
-            runCurrent()
-
-            verify(kosmos.mockEduMetricsLogger)
-                .logContextualEducationTriggered(gestureType, EducationUiType.Toast)
-        }
-
-    @Test
-    fun logMetricsForNotificationEducation() =
-        testScope.runTest {
-            triggerMaxEducationSignals(gestureType)
-            runCurrent()
-
-            eduClock.offset(minDurationForNextEdu)
-            triggerMaxEducationSignals(gestureType)
-            runCurrent()
-
-            verify(kosmos.mockEduMetricsLogger)
-                .logContextualEducationTriggered(gestureType, EducationUiType.Notification)
-        }
-
-    @After
-    fun clear() {
-        testScope.launch { tutorialSchedulerRepository.clear() }
-    }
-
-    private suspend fun triggerMaxEducationSignals(gestureType: GestureType) {
-        // Increment max number of signal to try triggering education
-        for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) {
-            contextualEduInteractor.incrementSignalCount(gestureType)
-        }
-    }
-
-    private fun TestScope.setIsAnyTouchpadConnected(isConnected: Boolean) {
-        touchpadRepository.setIsAnyTouchpadConnected(isConnected)
-        runCurrent()
-    }
-
-    private fun TestScope.setIsAnyKeyboardConnected(isConnected: Boolean) {
-        keyboardRepository.setIsAnyKeyboardConnected(isConnected)
-        runCurrent()
-    }
-
     private fun setUpForDeviceConnection() {
         touchpadRepository.setIsAnyTouchpadConnected(true)
         keyboardRepository.setIsAnyKeyboardConnected(true)
@@ -458,13 +167,12 @@
         return listenerCaptor.firstValue
     }
 
-    companion object {
-        private val USER_INFOS = listOf(UserInfo(101, "Second User", 0))
-
-        @JvmStatic
-        @Parameters(name = "{0}")
-        fun getGestureTypes(): List<GestureType> {
-            return listOf(BACK, HOME, OVERVIEW, ALL_APPS)
+    private fun TestScope.triggerEducation(gestureType: GestureType) {
+        // Increment max number of signal to try triggering education
+        for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) {
+            val listener = getOverviewProxyListener()
+            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
         }
+        runCurrent()
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
index d88d69d..d2317e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
@@ -22,7 +22,10 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.filter
 import androidx.compose.ui.test.hasContentDescription
 import androidx.compose.ui.test.junit4.ComposeContentTestRule
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -182,11 +185,14 @@
     }
 
     private fun ComposeContentTestRule.assertTileGridContainsExactly(specs: List<String>) {
-        onNodeWithTag(CURRENT_TILES_GRID_TEST_TAG).onChildren().apply {
-            fetchSemanticsNodes().forEachIndexed { index, _ ->
-                get(index).assert(hasContentDescription(specs[index]))
+        onNodeWithTag(CURRENT_TILES_GRID_TEST_TAG)
+            .onChildren()
+            .filter(SemanticsMatcher.keyIsDefined(SemanticsProperties.ContentDescription))
+            .apply {
+                fetchSemanticsNodes().forEachIndexed { index, _ ->
+                    get(index).assert(hasContentDescription(specs[index]))
+                }
             }
-        }
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
index 9a924ed..d090c01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
@@ -22,8 +22,10 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.qs.QSHost
 import com.android.systemui.qs.QsEventLogger
+import com.android.systemui.qs.flags.QsInCompose.isEnabled
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIconWithRes
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.BluetoothController
 import com.android.systemui.util.mockito.any
@@ -81,7 +83,7 @@
                 qsLogger,
                 bluetoothController,
                 featureFlags,
-                bluetoothTileDialogViewModel
+                bluetoothTileDialogViewModel,
             )
 
         tile.initialize()
@@ -109,8 +111,7 @@
 
         tile.handleUpdateState(state, /* arg= */ null)
 
-        assertThat(state.icon)
-            .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_bluetooth_icon_off))
+        assertThat(state.icon).isEqualTo(createExpectedIcon(R.drawable.qs_bluetooth_icon_off))
     }
 
     @Test
@@ -121,8 +122,7 @@
 
         tile.handleUpdateState(state, /* arg= */ null)
 
-        assertThat(state.icon)
-            .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_bluetooth_icon_off))
+        assertThat(state.icon).isEqualTo(createExpectedIcon(R.drawable.qs_bluetooth_icon_off))
     }
 
     @Test
@@ -133,8 +133,7 @@
 
         tile.handleUpdateState(state, /* arg= */ null)
 
-        assertThat(state.icon)
-            .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_bluetooth_icon_on))
+        assertThat(state.icon).isEqualTo(createExpectedIcon(R.drawable.qs_bluetooth_icon_on))
     }
 
     @Test
@@ -145,8 +144,7 @@
 
         tile.handleUpdateState(state, /* arg= */ null)
 
-        assertThat(state.icon)
-            .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_bluetooth_icon_search))
+        assertThat(state.icon).isEqualTo(createExpectedIcon(R.drawable.qs_bluetooth_icon_search))
     }
 
     @Test
@@ -161,11 +159,10 @@
             .isEqualTo(
                 mContext.getString(
                     R.string.quick_settings_bluetooth_secondary_label_battery_level,
-                    Utils.formatPercentage(50)
+                    Utils.formatPercentage(50),
                 )
             )
-        verify(bluetoothController)
-            .addOnMetadataChangedListener(eq(cachedDevice), any(), any())
+        verify(bluetoothController).addOnMetadataChangedListener(eq(cachedDevice), any(), any())
     }
 
     @Test
@@ -186,7 +183,7 @@
             .isEqualTo(
                 mContext.getString(
                     R.string.quick_settings_bluetooth_secondary_label_battery_level,
-                    Utils.formatPercentage(25)
+                    Utils.formatPercentage(25),
                 )
             )
         verify(bluetoothController, times(1))
@@ -197,7 +194,7 @@
     fun handleClick_hasSatelliteFeatureButNoQsTileDialogAndClickIsProcessing_doNothing() {
         mSetFlagsRule.enableFlags(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
         `when`(featureFlags.isEnabled(com.android.systemui.flags.Flags.BLUETOOTH_QS_TILE_DIALOG))
-                .thenReturn(false)
+            .thenReturn(false)
         `when`(clickJob.isCompleted).thenReturn(false)
         tile.mClickJob = clickJob
 
@@ -210,7 +207,7 @@
     fun handleClick_noSatelliteFeatureAndNoQsTileDialog_directSetBtEnable() {
         mSetFlagsRule.disableFlags(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
         `when`(featureFlags.isEnabled(com.android.systemui.flags.Flags.BLUETOOTH_QS_TILE_DIALOG))
-                .thenReturn(false)
+            .thenReturn(false)
 
         tile.handleClick(null)
 
@@ -221,7 +218,7 @@
     fun handleClick_noSatelliteFeatureButHasQsTileDialog_showDialog() {
         mSetFlagsRule.disableFlags(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
         `when`(featureFlags.isEnabled(com.android.systemui.flags.Flags.BLUETOOTH_QS_TILE_DIALOG))
-                .thenReturn(true)
+            .thenReturn(true)
 
         tile.handleClick(null)
 
@@ -265,7 +262,7 @@
         qsLogger: QSLogger,
         bluetoothController: BluetoothController,
         featureFlags: FeatureFlagsClassic,
-        bluetoothTileDialogViewModel: BluetoothTileDialogViewModel
+        bluetoothTileDialogViewModel: BluetoothTileDialogViewModel,
     ) :
         BluetoothTile(
             qsHost,
@@ -279,13 +276,13 @@
             qsLogger,
             bluetoothController,
             featureFlags,
-            bluetoothTileDialogViewModel
+            bluetoothTileDialogViewModel,
         ) {
         var restrictionChecked: String? = null
 
         override fun checkIfRestrictionEnforcedByAdminOnly(
             state: QSTile.State?,
-            userRestriction: String?
+            userRestriction: String?,
         ) {
             restrictionChecked = userRestriction
         }
@@ -321,7 +318,7 @@
     fun listenToDeviceMetadata(
         state: QSTile.BooleanState,
         cachedDevice: CachedBluetoothDevice,
-        batteryLevel: Int
+        batteryLevel: Int,
     ) {
         val btDevice = mock<BluetoothDevice>()
         whenever(cachedDevice.device).thenReturn(btDevice)
@@ -332,4 +329,12 @@
         addConnectedDevice(cachedDevice)
         tile.handleUpdateState(state, /* arg= */ null)
     }
+
+    private fun createExpectedIcon(resId: Int): QSTile.Icon {
+        return if (isEnabled) {
+            DrawableIconWithRes(mContext.getDrawable(resId), resId)
+        } else {
+            QSTileImpl.ResourceIcon.get(resId)
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
index 6a43a61..56b7631 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
@@ -29,7 +29,6 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.MetricsLogger
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.animation.Expandable
@@ -39,14 +38,18 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.qs.QSHost
 import com.android.systemui.qs.QsEventLogger
+import com.android.systemui.qs.flags.QsInCompose.isEnabled
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIconWithRes
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.ZenModeController
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.util.settings.SecureSettings
 import com.google.common.truth.Truth.assertThat
+import java.io.File
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
@@ -55,9 +58,8 @@
 import org.mockito.Mockito.anyBoolean
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import java.io.File
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -70,41 +72,29 @@
         private const val KEY = Settings.Secure.ZEN_DURATION
     }
 
-    @Mock
-    private lateinit var qsHost: QSHost
+    @Mock private lateinit var qsHost: QSHost
 
-    @Mock
-    private lateinit var metricsLogger: MetricsLogger
+    @Mock private lateinit var metricsLogger: MetricsLogger
 
-    @Mock
-    private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
 
-    @Mock
-    private lateinit var activityStarter: ActivityStarter
+    @Mock private lateinit var activityStarter: ActivityStarter
 
-    @Mock
-    private lateinit var qsLogger: QSLogger
+    @Mock private lateinit var qsLogger: QSLogger
 
-    @Mock
-    private lateinit var uiEventLogger: QsEventLogger
+    @Mock private lateinit var uiEventLogger: QsEventLogger
 
-    @Mock
-    private lateinit var zenModeController: ZenModeController
+    @Mock private lateinit var zenModeController: ZenModeController
 
-    @Mock
-    private lateinit var sharedPreferences: SharedPreferences
+    @Mock private lateinit var sharedPreferences: SharedPreferences
 
-    @Mock
-    private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator
+    @Mock private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator
 
-    @Mock
-    private lateinit var hostDialog: Dialog
+    @Mock private lateinit var hostDialog: Dialog
 
-    @Mock
-    private lateinit var expandable: Expandable
+    @Mock private lateinit var expandable: Expandable
 
-    @Mock
-    private lateinit var controller: DialogTransitionAnimator.Controller
+    @Mock private lateinit var controller: DialogTransitionAnimator.Controller
 
     private lateinit var secureSettings: SecureSettings
     private lateinit var testableLooper: TestableLooper
@@ -118,31 +108,32 @@
 
         whenever(qsHost.userId).thenReturn(DEFAULT_USER)
 
-        val wrappedContext = object : ContextWrapper(
-                ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
-        ) {
-            override fun getSharedPreferences(file: File?, mode: Int): SharedPreferences {
-                return sharedPreferences
+        val wrappedContext =
+            object :
+                ContextWrapper(ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)) {
+                override fun getSharedPreferences(file: File?, mode: Int): SharedPreferences {
+                    return sharedPreferences
+                }
             }
-        }
         whenever(qsHost.context).thenReturn(wrappedContext)
         whenever(expandable.dialogTransitionController(any())).thenReturn(controller)
 
-        tile = DndTile(
-            qsHost,
-            uiEventLogger,
-            testableLooper.looper,
-            Handler(testableLooper.looper),
-            FalsingManagerFake(),
-            metricsLogger,
-            statusBarStateController,
-            activityStarter,
-            qsLogger,
-            zenModeController,
-            sharedPreferences,
-            secureSettings,
-            mDialogTransitionAnimator
-        )
+        tile =
+            DndTile(
+                qsHost,
+                uiEventLogger,
+                testableLooper.looper,
+                Handler(testableLooper.looper),
+                FalsingManagerFake(),
+                metricsLogger,
+                statusBarStateController,
+                activityStarter,
+                qsLogger,
+                zenModeController,
+                sharedPreferences,
+                secureSettings,
+                mDialogTransitionAnimator,
+            )
     }
 
     @After
@@ -222,7 +213,7 @@
 
         tile.handleUpdateState(state, /* arg= */ null)
 
-        assertThat(state.icon).isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_dnd_icon_off))
+        assertThat(state.icon).isEqualTo(createExpectedIcon(R.drawable.qs_dnd_icon_off))
     }
 
     @Test
@@ -232,6 +223,14 @@
 
         tile.handleUpdateState(state, /* arg= */ null)
 
-        assertThat(state.icon).isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_dnd_icon_on))
+        assertThat(state.icon).isEqualTo(createExpectedIcon(R.drawable.qs_dnd_icon_on))
+    }
+
+    private fun createExpectedIcon(resId: Int): QSTile.Icon {
+        return if (isEnabled) {
+            DrawableIconWithRes(mContext.getDrawable(resId), resId)
+        } else {
+            QSTileImpl.ResourceIcon.get(resId)
+        }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java
index 190d80f..f043f63 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java
@@ -45,9 +45,11 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.QsEventLogger;
+import com.android.systemui.qs.flags.QsInCompose;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.res.R;
@@ -246,13 +248,13 @@
         dockIntent.putExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_DESK);
         receiver.onReceive(mContext, dockIntent);
         mTestableLooper.processAllMessages();
-        assertEquals(QSTileImpl.ResourceIcon.get(R.drawable.ic_qs_screen_saver),
+        assertEquals(createExpectedIcon(R.drawable.ic_qs_screen_saver),
                 dockedTile.getState().icon);
 
         dockIntent.putExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED);
         receiver.onReceive(mContext, dockIntent);
         mTestableLooper.processAllMessages();
-        assertEquals(QSTileImpl.ResourceIcon.get(R.drawable.ic_qs_screen_saver_undocked),
+        assertEquals(createExpectedIcon(R.drawable.ic_qs_screen_saver_undocked),
                 dockedTile.getState().icon);
 
         destroyTile(dockedTile);
@@ -268,6 +270,14 @@
         mTestableLooper.processAllMessages();
     }
 
+    private QSTile.Icon createExpectedIcon(int resId) {
+        if (QsInCompose.isEnabled()) {
+            return new QSTileImpl.DrawableIconWithRes(mContext.getDrawable(resId), resId);
+        } else {
+            return QSTileImpl.ResourceIcon.get(resId);
+        }
+    }
+
     private DreamTile constructTileForTest(boolean dreamSupported,
             boolean dreamOnlyEnabledForSystemUser) {
         return new DreamTile(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java
index 5bd6944..2b4cf5d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java
@@ -38,6 +38,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.QsEventLogger;
+import com.android.systemui.qs.flags.QsInCompose;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.res.R;
@@ -144,7 +145,7 @@
         mTile.handleUpdateState(state, /* arg= */ null);
 
         assertThat(state.icon)
-                .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_hotspot_icon_off));
+                .isEqualTo(createExpectedIcon(R.drawable.qs_hotspot_icon_off));
     }
 
     @Test
@@ -156,7 +157,7 @@
         mTile.handleUpdateState(state, /* arg= */ null);
 
         assertThat(state.icon)
-                .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_hotspot_icon_search));
+                .isEqualTo(createExpectedIcon(R.drawable.qs_hotspot_icon_search));
     }
 
     @Test
@@ -168,6 +169,14 @@
         mTile.handleUpdateState(state, /* arg= */ null);
 
         assertThat(state.icon)
-                .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_hotspot_icon_on));
+                .isEqualTo(createExpectedIcon(R.drawable.qs_hotspot_icon_on));
+    }
+
+    private QSTile.Icon createExpectedIcon(int resId) {
+        if (QsInCompose.isEnabled()) {
+            return new QSTileImpl.DrawableIconWithRes(mContext.getDrawable(resId), resId);
+        } else {
+            return QSTileImpl.ResourceIcon.get(resId);
+        }
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
index 1df3ef4..1021169 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
@@ -17,9 +17,11 @@
 package com.android.systemui.education.data.repository
 
 import com.android.systemui.kosmos.Kosmos
+import java.time.Duration
 import java.time.Instant
 
 var Kosmos.contextualEducationRepository: FakeContextualEducationRepository by
     Kosmos.Fixture { FakeContextualEducationRepository() }
 
-var Kosmos.fakeEduClock: FakeEduClock by Kosmos.Fixture { FakeEduClock(Instant.MIN) }
+var Kosmos.fakeEduClock: FakeEduClock by
+    Kosmos.Fixture { FakeEduClock(Instant.ofEpochSecond(Duration.ofDays(30).seconds)) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
index 597d52d..bc1c60c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.qs.tiles.base.interactor
 
+import com.android.systemui.plugins.qs.TileDetailsViewModel
+import com.android.systemui.qs.FakeTileDetailsViewModel
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
 
@@ -31,4 +33,7 @@
     override suspend fun handleInput(input: QSTileInput<T>) {
         mutex.withLock { mutableInputs.add(input) }
     }
+
+    override var detailsViewModel: TileDetailsViewModel? =
+        FakeTileDetailsViewModel("FakeQSTileUserActionInteractor")
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt
index c8ba551..34661ce 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.statusbar.notification.domain.interactor.notificationsSoundPolicyInteractor
 import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
 import com.android.systemui.volume.dialog.ringer.domain.volumeDialogRingerInteractor
 import com.android.systemui.volume.dialog.shared.volumeDialogLogger
@@ -31,7 +32,8 @@
             applicationContext = applicationContext,
             backgroundDispatcher = testDispatcher,
             coroutineScope = applicationCoroutineScope,
-            interactor = volumeDialogRingerInteractor,
+            soundPolicyInteractor = notificationsSoundPolicyInteractor,
+            ringerInteractor = volumeDialogRingerInteractor,
             vibrator = vibratorHelper,
             volumeDialogLogger = volumeDialogLogger,
             visibilityInteractor = volumeDialogVisibilityInteractor,
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index eba9a25..bd7a0ac 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -449,6 +449,8 @@
     private AtomicBoolean mIsSatelliteEnabled;
     private AtomicBoolean mWasSatelliteEnabledNotified;
 
+    private final int mPid = Process.myPid();
+
     /**
      * Per-phone map of precise data connection state. The key of the map is the pair of transport
      * type and APN setting. This is the cache to prevent redundant callbacks to the listeners.
@@ -1441,7 +1443,17 @@
                 }
                 if (events.contains(TelephonyCallback.EVENT_CALL_ATTRIBUTES_CHANGED)) {
                     try {
-                        r.callback.onCallStatesChanged(mCallStateLists.get(r.phoneId));
+                        if (Flags.passCopiedCallStateList()) {
+                            List<CallState> callList;
+                            if (r.callerPid == mPid) {
+                                callList = List.copyOf(mCallStateLists.get(r.phoneId));
+                            } else {
+                                callList = mCallStateLists.get(r.phoneId);
+                            }
+                            r.callback.onCallStatesChanged(callList);
+                        } else {
+                            r.callback.onCallStatesChanged(mCallStateLists.get(r.phoneId));
+                        }
                     } catch (RemoteException ex) {
                         remove(r.binder);
                     }
@@ -2573,12 +2585,25 @@
                 }
 
                 if (notifyCallState) {
+                    List<CallState> copyList = null;
                     for (Record r : mRecords) {
                         if (r.matchTelephonyCallbackEvent(
                                 TelephonyCallback.EVENT_CALL_ATTRIBUTES_CHANGED)
                                 && idMatch(r, subId, phoneId)) {
+                            // If listener is in the same process, original instance can be passed
+                            // to the listener via AIDL without serialization/de-serialization. We
+                            // will pass the copied list. Since the element is newly created instead
+                            // of modification for the change, we can use shallow copy for this.
                             try {
-                                r.callback.onCallStatesChanged(mCallStateLists.get(phoneId));
+                                if (Flags.passCopiedCallStateList()) {
+                                    if (r.callerPid == mPid && copyList == null) {
+                                        copyList = List.copyOf(mCallStateLists.get(phoneId));
+                                    }
+                                    r.callback.onCallStatesChanged(copyList == null
+                                            ? mCallStateLists.get(phoneId) : copyList);
+                                } else {
+                                    r.callback.onCallStatesChanged(mCallStateLists.get(phoneId));
+                                }
                             } catch (RemoteException ex) {
                                 mRemoveList.add(r.binder);
                             }
@@ -2906,13 +2931,21 @@
                     log("There is no active call to report CallQuality");
                     return;
                 }
-
+                List<CallState> copyList = null;
                 for (Record r : mRecords) {
                     if (r.matchTelephonyCallbackEvent(
                             TelephonyCallback.EVENT_CALL_ATTRIBUTES_CHANGED)
                             && idMatch(r, subId, phoneId)) {
                         try {
-                            r.callback.onCallStatesChanged(mCallStateLists.get(phoneId));
+                            if (Flags.passCopiedCallStateList()) {
+                                if (r.callerPid == mPid && copyList == null) {
+                                    copyList = List.copyOf(mCallStateLists.get(phoneId));
+                                }
+                                r.callback.onCallStatesChanged(copyList == null
+                                        ? mCallStateLists.get(phoneId) : copyList);
+                            } else {
+                                r.callback.onCallStatesChanged(mCallStateLists.get(phoneId));
+                            }
                         } catch (RemoteException ex) {
                             mRemoveList.add(r.binder);
                         }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 06883e8..cd929c1 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -361,6 +361,8 @@
 import android.os.incremental.IIncrementalService;
 import android.os.incremental.IncrementalManager;
 import android.os.incremental.IncrementalMetrics;
+import android.os.instrumentation.IOffsetCallback;
+import android.os.instrumentation.MethodDescriptor;
 import android.os.storage.IStorageManager;
 import android.os.storage.StorageManager;
 import android.provider.DeviceConfig;
@@ -509,6 +511,7 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.Set;
 import java.util.UUID;
@@ -18042,6 +18045,26 @@
         }
 
         @Override
+        public void getExecutableMethodFileOffsets(@NonNull String processName,
+                int pid, int uid, @NonNull MethodDescriptor methodDescriptor,
+                @NonNull IOffsetCallback callback) {
+            final IApplicationThread thread;
+            synchronized (ActivityManagerService.this) {
+                ProcessRecord record = mProcessList.getProcessRecordLocked(processName, uid);
+                if (record == null || record.getPid() != pid) {
+                    throw new NoSuchElementException();
+                }
+                thread = record.getThread();
+            }
+            try {
+                thread.getExecutableMethodFileOffsets(methodDescriptor, callback);
+            } catch (RemoteException e) {
+                throw new RuntimeException(
+                        "IApplicationThread.getExecutableMethodFileOffsets failed", e);
+            }
+        }
+
+        @Override
         public void addCreatorToken(Intent intent, String creatorPackage) {
             ActivityManagerService.this.addCreatorToken(intent, creatorPackage);
         }
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index 0b47a61..d916eda 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -798,7 +798,7 @@
             throws RemoteException {
         super.registerEndpoint_enforcePermission();
         if (mEndpointManager == null) {
-            Log.e(TAG, "ContextHubService.registerEndpoint: endpoint manager failed to initialize");
+            Log.e(TAG, "Endpoint manager failed to initialize");
             throw new UnsupportedOperationException("Endpoint registration is not supported");
         }
         return mEndpointManager.registerEndpoint(pendingHubEndpointInfo, callback);
@@ -809,7 +809,8 @@
     public void registerEndpointDiscoveryCallbackId(
             long endpointId, IContextHubEndpointDiscoveryCallback callback) throws RemoteException {
         super.registerEndpointDiscoveryCallbackId_enforcePermission();
-        // TODO(b/375487784): Implement this
+        checkEndpointDiscoveryPreconditions();
+        mHubInfoRegistry.registerEndpointDiscoveryCallback(endpointId, callback);
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@@ -818,7 +819,8 @@
             String serviceDescriptor, IContextHubEndpointDiscoveryCallback callback)
             throws RemoteException {
         super.registerEndpointDiscoveryCallbackDescriptor_enforcePermission();
-        // TODO(b/375487784): Implement this
+        checkEndpointDiscoveryPreconditions();
+        mHubInfoRegistry.registerEndpointDiscoveryCallback(serviceDescriptor, callback);
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@@ -826,7 +828,15 @@
     public void unregisterEndpointDiscoveryCallback(IContextHubEndpointDiscoveryCallback callback)
             throws RemoteException {
         super.unregisterEndpointDiscoveryCallback_enforcePermission();
-        // TODO(b/375487784): Implement this
+        checkEndpointDiscoveryPreconditions();
+        mHubInfoRegistry.unregisterEndpointDiscoveryCallback(callback);
+    }
+
+    private void checkEndpointDiscoveryPreconditions() {
+        if (mHubInfoRegistry == null) {
+            Log.e(TAG, "Hub endpoint registry failed to initialize");
+            throw new UnsupportedOperationException("Endpoint discovery is not supported");
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/location/contexthub/HubInfoRegistry.java b/services/core/java/com/android/server/location/contexthub/HubInfoRegistry.java
index b912492..6f5f191 100644
--- a/services/core/java/com/android/server/location/contexthub/HubInfoRegistry.java
+++ b/services/core/java/com/android/server/location/contexthub/HubInfoRegistry.java
@@ -18,7 +18,9 @@
 
 import android.hardware.contexthub.HubEndpointInfo;
 import android.hardware.contexthub.HubServiceInfo;
+import android.hardware.contexthub.IContextHubEndpointDiscoveryCallback;
 import android.hardware.location.HubInfo;
+import android.os.DeadObjectException;
 import android.os.RemoteException;
 import android.util.ArrayMap;
 import android.util.IndentingPrintWriter;
@@ -29,6 +31,9 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.BiConsumer;
 
 class HubInfoRegistry implements ContextHubHalEndpointCallback.IEndpointLifecycleCallback {
     private static final String TAG = "HubInfoRegistry";
@@ -43,6 +48,56 @@
     private final ArrayMap<HubEndpointInfo.HubEndpointIdentifier, HubEndpointInfo>
             mHubEndpointInfos = new ArrayMap<>();
 
+    /**
+     * A wrapper class that is used to store arguments to
+     * ContextHubManager.registerEndpointCallback.
+     */
+    private static class DiscoveryCallback {
+        private final IContextHubEndpointDiscoveryCallback mCallback;
+        private final Optional<Long> mEndpointId;
+        private final Optional<String> mServiceDescriptor;
+
+        DiscoveryCallback(IContextHubEndpointDiscoveryCallback callback, long endpointId) {
+            mCallback = callback;
+            mEndpointId = Optional.of(endpointId);
+            mServiceDescriptor = Optional.empty();
+        }
+
+        DiscoveryCallback(IContextHubEndpointDiscoveryCallback callback, String serviceDescriptor) {
+            mCallback = callback;
+            mEndpointId = Optional.empty();
+            mServiceDescriptor = Optional.of(serviceDescriptor);
+        }
+
+        public IContextHubEndpointDiscoveryCallback getCallback() {
+            return mCallback;
+        }
+
+        /**
+         * @param info The hub endpoint info to check
+         * @return true if info matches
+         */
+        public boolean isMatch(HubEndpointInfo info) {
+            if (mEndpointId.isPresent()) {
+                return mEndpointId.get() == info.getIdentifier().getEndpoint();
+            }
+            if (mServiceDescriptor.isPresent()) {
+                for (HubServiceInfo serviceInfo : info.getServiceInfoCollection()) {
+                    if (mServiceDescriptor.get().equals(serviceInfo.getServiceDescriptor())) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+    }
+
+    /* The list of discovery callbacks registered with the service */
+    @GuardedBy("mCallbackLock")
+    private final List<DiscoveryCallback> mEndpointDiscoveryCallbacks = new ArrayList<>();
+
+    private final Object mCallbackLock = new Object();
+
     HubInfoRegistry(IContextHubWrapper contextHubWrapper) {
         mContextHubWrapper = contextHubWrapper;
         refreshCachedHubs();
@@ -109,16 +164,50 @@
                 mHubEndpointInfos.put(endpointInfo.getIdentifier(), endpointInfo);
             }
         }
+
+        invokeForMatchingEndpoints(
+                endpointInfos,
+                (cb, infoList) -> {
+                    try {
+                        cb.onEndpointsStarted(infoList);
+                    } catch (RemoteException e) {
+                        if (e instanceof DeadObjectException) {
+                            Log.w(TAG, "onEndpointStarted: callback died, unregistering");
+                            unregisterEndpointDiscoveryCallback(cb);
+                        } else {
+                            Log.e(TAG, "Exception while calling onEndpointsStarted", e);
+                        }
+                    }
+                });
     }
 
     @Override
     public void onEndpointStopped(
             HubEndpointInfo.HubEndpointIdentifier[] endpointIds, byte reason) {
+        ArrayList<HubEndpointInfo> removedInfoList = new ArrayList<>();
         synchronized (mLock) {
             for (HubEndpointInfo.HubEndpointIdentifier endpointId : endpointIds) {
-                mHubEndpointInfos.remove(endpointId);
+                HubEndpointInfo info = mHubEndpointInfos.remove(endpointId);
+                if (info != null) {
+                    removedInfoList.add(info);
+                }
             }
         }
+
+        invokeForMatchingEndpoints(
+                removedInfoList.toArray(new HubEndpointInfo[removedInfoList.size()]),
+                (cb, infoList) -> {
+                    try {
+                        cb.onEndpointsStopped(infoList, reason);
+                    } catch (RemoteException e) {
+                        if (e instanceof DeadObjectException) {
+                            Log.w(TAG, "onEndpointStopped: callback died, unregistering");
+                            unregisterEndpointDiscoveryCallback(cb);
+                        } else {
+                            Log.e(TAG, "Exception while calling onEndpointsStopped", e);
+                        }
+                    }
+                });
     }
 
     /** Return a list of {@link HubEndpointInfo} that represents endpoints with the matching id. */
@@ -151,6 +240,77 @@
         return searchResult;
     }
 
+    /* package */
+    void registerEndpointDiscoveryCallback(
+            long endpointId, IContextHubEndpointDiscoveryCallback callback) {
+        Objects.requireNonNull(callback, "callback cannot be null");
+        synchronized (mCallbackLock) {
+            checkCallbackAlreadyRegistered(callback);
+            mEndpointDiscoveryCallbacks.add(new DiscoveryCallback(callback, endpointId));
+        }
+    }
+
+    /* package */
+    void registerEndpointDiscoveryCallback(
+            String serviceDescriptor, IContextHubEndpointDiscoveryCallback callback) {
+        Objects.requireNonNull(callback, "callback cannot be null");
+        synchronized (mCallbackLock) {
+            checkCallbackAlreadyRegistered(callback);
+            mEndpointDiscoveryCallbacks.add(new DiscoveryCallback(callback, serviceDescriptor));
+        }
+    }
+
+    /* package */
+    void unregisterEndpointDiscoveryCallback(IContextHubEndpointDiscoveryCallback callback) {
+        Objects.requireNonNull(callback, "callback cannot be null");
+        synchronized (mCallbackLock) {
+            for (DiscoveryCallback discoveryCallback : mEndpointDiscoveryCallbacks) {
+                if (discoveryCallback.getCallback().asBinder() == callback.asBinder()) {
+                    mEndpointDiscoveryCallbacks.remove(discoveryCallback);
+                    break;
+                }
+            }
+        }
+    }
+
+    private void checkCallbackAlreadyRegistered(
+            IContextHubEndpointDiscoveryCallback callback) {
+        synchronized (mCallbackLock) {
+            for (DiscoveryCallback discoveryCallback : mEndpointDiscoveryCallbacks) {
+                if (discoveryCallback.mCallback.asBinder() == callback.asBinder()) {
+                    throw new IllegalArgumentException("Callback is already registered");
+                }
+            }
+        }
+    }
+
+    /**
+     * Iterates through all registered discovery callbacks and invokes a given callback for those
+     * that match the endpoints the callback is targeted for.
+     *
+     * @param endpointInfos The list of endpoint infos to check for a match.
+     * @param consumer The callback to invoke, which consumes the callback object and the list of
+     *     matched endpoint infos.
+     */
+    private void invokeForMatchingEndpoints(
+            HubEndpointInfo[] endpointInfos,
+            BiConsumer<IContextHubEndpointDiscoveryCallback, HubEndpointInfo[]> consumer) {
+        synchronized (mCallbackLock) {
+            for (DiscoveryCallback discoveryCallback : mEndpointDiscoveryCallbacks) {
+                ArrayList<HubEndpointInfo> infoList = new ArrayList<>();
+                for (HubEndpointInfo endpointInfo : endpointInfos) {
+                    if (discoveryCallback.isMatch(endpointInfo)) {
+                        infoList.add(endpointInfo);
+                    }
+                }
+
+                consumer.accept(
+                        discoveryCallback.getCallback(),
+                        infoList.toArray(new HubEndpointInfo[infoList.size()]));
+            }
+        }
+    }
+
     void dump(IndentingPrintWriter ipw) {
         synchronized (mLock) {
             dumpLocked(ipw);
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index 849751b..1673b8e 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -32,6 +32,7 @@
 import android.media.quality.SoundProfile;
 import android.media.quality.SoundProfileHandle;
 import android.os.PersistableBundle;
+import android.os.UserHandle;
 import android.util.Log;
 
 import com.android.server.SystemService;
@@ -81,7 +82,7 @@
     private final class BinderService extends IMediaQualityManager.Stub {
 
         @Override
-        public PictureProfile createPictureProfile(PictureProfile pp, int userId) {
+        public PictureProfile createPictureProfile(PictureProfile pp, UserHandle user) {
             SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
 
             ContentValues values = new ContentValues();
@@ -100,12 +101,12 @@
         }
 
         @Override
-        public void updatePictureProfile(String id, PictureProfile pp, int userId) {
+        public void updatePictureProfile(String id, PictureProfile pp, UserHandle user) {
             // TODO: implement
         }
 
         @Override
-        public void removePictureProfile(String id, int userId) {
+        public void removePictureProfile(String id, UserHandle user) {
             Long intId = mPictureProfileTempIdMap.inverse().get(id);
             if (intId != null) {
                 SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
@@ -118,7 +119,8 @@
         }
 
         @Override
-        public PictureProfile getPictureProfile(int type, String name, int userId) {
+        public PictureProfile getPictureProfile(int type, String name, boolean includeParams,
+                UserHandle user) {
             String selection = BaseParameters.PARAMETER_TYPE + " = ? AND "
                     + BaseParameters.PARAMETER_NAME + " = ?";
             String[] selectionArguments = {Integer.toString(type), name};
@@ -144,7 +146,8 @@
         }
 
         @Override
-        public List<PictureProfile> getPictureProfilesByPackage(String packageName, int userId) {
+        public List<PictureProfile> getPictureProfilesByPackage(
+                String packageName, boolean includeParams, UserHandle user) {
             String selection = BaseParameters.PARAMETER_PACKAGE + " = ?";
             String[] selectionArguments = {packageName};
             return getPictureProfilesBasedOnConditions(getAllMediaProfileColumns(), selection,
@@ -152,18 +155,19 @@
         }
 
         @Override
-        public List<PictureProfile> getAvailablePictureProfiles(int userId) {
+        public List<PictureProfile> getAvailablePictureProfiles(
+                boolean includeParams, UserHandle user) {
             return new ArrayList<>();
         }
 
         @Override
-        public boolean setDefaultPictureProfile(String profileId, int userId) {
+        public boolean setDefaultPictureProfile(String profileId, UserHandle user) {
             // TODO: pass the profile ID to MediaQuality HAL when ready.
             return false;
         }
 
         @Override
-        public List<String> getPictureProfilePackageNames(int userId) {
+        public List<String> getPictureProfilePackageNames(UserHandle user) {
             String [] column = {BaseParameters.PARAMETER_PACKAGE};
             List<PictureProfile> pictureProfiles = getPictureProfilesBasedOnConditions(column,
                     null, null);
@@ -174,17 +178,17 @@
         }
 
         @Override
-        public List<PictureProfileHandle> getPictureProfileHandle(String[] id, int userId) {
+        public List<PictureProfileHandle> getPictureProfileHandle(String[] id, UserHandle user) {
             return new ArrayList<>();
         }
 
         @Override
-        public List<SoundProfileHandle> getSoundProfileHandle(String[] id, int userId) {
+        public List<SoundProfileHandle> getSoundProfileHandle(String[] id, UserHandle user) {
             return new ArrayList<>();
         }
 
         @Override
-        public SoundProfile createSoundProfile(SoundProfile sp, int userId) {
+        public SoundProfile createSoundProfile(SoundProfile sp, UserHandle user) {
             SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
 
             ContentValues values = new ContentValues();
@@ -203,12 +207,12 @@
         }
 
         @Override
-        public void updateSoundProfile(String id, SoundProfile pp, int userId) {
+        public void updateSoundProfile(String id, SoundProfile pp, UserHandle user) {
             // TODO: implement
         }
 
         @Override
-        public void removeSoundProfile(String id, int userId) {
+        public void removeSoundProfile(String id, UserHandle user) {
             Long intId = mSoundProfileTempIdMap.inverse().get(id);
             if (intId != null) {
                 SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
@@ -221,9 +225,10 @@
         }
 
         @Override
-        public SoundProfile getSoundProfile(int type, String id, int userId) {
+        public SoundProfile getSoundProfile(int type, String id, boolean includeParams,
+                UserHandle user) {
             String selection = BaseParameters.PARAMETER_TYPE + " = ? AND "
-                    + BaseParameters.PARAMETER_NAME + " = ?";
+                    + BaseParameters.PARAMETER_ID + " = ?";
             String[] selectionArguments = {String.valueOf(type), id};
 
             try (
@@ -247,7 +252,8 @@
         }
 
         @Override
-        public List<SoundProfile> getSoundProfilesByPackage(String packageName, int userId) {
+        public List<SoundProfile> getSoundProfilesByPackage(
+                String packageName, boolean includeParams, UserHandle user) {
             String selection = BaseParameters.PARAMETER_PACKAGE + " = ?";
             String[] selectionArguments = {packageName};
             return getSoundProfilesBasedOnConditions(getAllMediaProfileColumns(), selection,
@@ -255,18 +261,19 @@
         }
 
         @Override
-        public List<SoundProfile> getAvailableSoundProfiles(int userId) {
+        public List<SoundProfile> getAvailableSoundProfiles(
+                boolean includeParams, UserHandle user) {
             return new ArrayList<>();
         }
 
         @Override
-        public boolean setDefaultSoundProfile(String profileId, int userId) {
+        public boolean setDefaultSoundProfile(String profileId, UserHandle user) {
             // TODO: pass the profile ID to MediaQuality HAL when ready.
             return false;
         }
 
         @Override
-        public List<String> getSoundProfilePackageNames(int userId) {
+        public List<String> getSoundProfilePackageNames(UserHandle user) {
             String [] column = {BaseParameters.PARAMETER_NAME};
             List<SoundProfile> soundProfiles = getSoundProfilesBasedOnConditions(column,
                     null, null);
@@ -456,70 +463,71 @@
         }
 
         @Override
-        public void setAmbientBacklightSettings(AmbientBacklightSettings settings, int userId) {
+        public void setAmbientBacklightSettings(
+                AmbientBacklightSettings settings, UserHandle user) {
         }
 
         @Override
-        public void setAmbientBacklightEnabled(boolean enabled, int userId) {
+        public void setAmbientBacklightEnabled(boolean enabled, UserHandle user) {
         }
 
         @Override
-        public List<ParamCapability> getParamCapabilities(List<String> names, int userId) {
+        public List<ParamCapability> getParamCapabilities(List<String> names, UserHandle user) {
             return new ArrayList<>();
         }
 
         @Override
-        public List<String> getPictureProfileAllowList(int userId) {
+        public List<String> getPictureProfileAllowList(UserHandle user) {
             return new ArrayList<>();
         }
 
         @Override
-        public void setPictureProfileAllowList(List<String> packages, int userId) {
+        public void setPictureProfileAllowList(List<String> packages, UserHandle user) {
         }
 
         @Override
-        public List<String> getSoundProfileAllowList(int userId) {
+        public List<String> getSoundProfileAllowList(UserHandle user) {
             return new ArrayList<>();
         }
 
         @Override
-        public void setSoundProfileAllowList(List<String> packages, int userId) {
+        public void setSoundProfileAllowList(List<String> packages, UserHandle user) {
         }
 
         @Override
-        public boolean isSupported(int userId) {
+        public boolean isSupported(UserHandle user) {
             return false;
         }
 
         @Override
-        public void setAutoPictureQualityEnabled(boolean enabled, int userId) {
+        public void setAutoPictureQualityEnabled(boolean enabled, UserHandle user) {
         }
 
         @Override
-        public boolean isAutoPictureQualityEnabled(int userId) {
+        public boolean isAutoPictureQualityEnabled(UserHandle user) {
             return false;
         }
 
         @Override
-        public void setSuperResolutionEnabled(boolean enabled, int userId) {
+        public void setSuperResolutionEnabled(boolean enabled, UserHandle user) {
         }
 
         @Override
-        public boolean isSuperResolutionEnabled(int userId) {
+        public boolean isSuperResolutionEnabled(UserHandle user) {
             return false;
         }
 
         @Override
-        public void setAutoSoundQualityEnabled(boolean enabled, int userId) {
+        public void setAutoSoundQualityEnabled(boolean enabled, UserHandle user) {
         }
 
         @Override
-        public boolean isAutoSoundQualityEnabled(int userId) {
+        public boolean isAutoSoundQualityEnabled(UserHandle user) {
             return false;
         }
 
         @Override
-        public boolean isAmbientBacklightEnabled(int userId) {
+        public boolean isAmbientBacklightEnabled(UserHandle user) {
             return false;
         }
     }
diff --git a/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java b/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java
index 8ec7160..871d12e 100644
--- a/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java
+++ b/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java
@@ -20,116 +20,100 @@
 import static android.content.Context.DYNAMIC_INSTRUMENTATION_SERVICE;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.annotation.PermissionManuallyEnforced;
+import android.annotation.RequiresPermission;
+import android.app.ActivityManagerInternal;
 import android.content.Context;
+import android.os.RemoteException;
 import android.os.instrumentation.ExecutableMethodFileOffsets;
 import android.os.instrumentation.IDynamicInstrumentationManager;
+import android.os.instrumentation.IOffsetCallback;
 import android.os.instrumentation.MethodDescriptor;
+import android.os.instrumentation.MethodDescriptorParser;
 import android.os.instrumentation.TargetProcess;
 
-import com.android.internal.annotations.VisibleForTesting;
+
+import com.android.server.LocalServices;
 import com.android.server.SystemService;
 
 import dalvik.system.VMDebug;
 
 import java.lang.reflect.Method;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+
 
 /**
  * System private implementation of the {@link IDynamicInstrumentationManager interface}.
  */
 public class DynamicInstrumentationManagerService extends SystemService {
+
+    private ActivityManagerInternal mAmInternal;
+
     public DynamicInstrumentationManagerService(@NonNull Context context) {
         super(context);
     }
 
     @Override
     public void onStart() {
+        mAmInternal = LocalServices.getService(ActivityManagerInternal.class);
         publishBinderService(DYNAMIC_INSTRUMENTATION_SERVICE, new BinderService());
     }
 
     private final class BinderService extends IDynamicInstrumentationManager.Stub {
         @Override
         @PermissionManuallyEnforced
-        public @Nullable ExecutableMethodFileOffsets getExecutableMethodFileOffsets(
-                @NonNull TargetProcess targetProcess, @NonNull MethodDescriptor methodDescriptor) {
+        @RequiresPermission(value = android.Manifest.permission.DYNAMIC_INSTRUMENTATION)
+        public void getExecutableMethodFileOffsets(
+                @NonNull TargetProcess targetProcess, @NonNull MethodDescriptor methodDescriptor,
+                @NonNull IOffsetCallback callback) {
             if (!com.android.art.flags.Flags.executableMethodFileOffsets()) {
                 throw new UnsupportedOperationException();
             }
             getContext().enforceCallingOrSelfPermission(
                     DYNAMIC_INSTRUMENTATION, "Caller must have DYNAMIC_INSTRUMENTATION permission");
+            Objects.requireNonNull(targetProcess.processName);
 
-            if (targetProcess.processName == null
-                    || !targetProcess.processName.equals("system_server")) {
-                throw new UnsupportedOperationException(
-                        "system_server is the only supported target process");
+            if (!targetProcess.processName.equals("system_server")) {
+                try {
+                    mAmInternal.getExecutableMethodFileOffsets(targetProcess.processName,
+                            targetProcess.pid, targetProcess.uid, methodDescriptor,
+                            new IOffsetCallback.Stub() {
+                                @Override
+                                public void onResult(ExecutableMethodFileOffsets result) {
+                                    try {
+                                        callback.onResult(result);
+                                    } catch (RemoteException e) {
+                                        /* ignore */
+                                    }
+                                }
+                            });
+                    return;
+                } catch (NoSuchElementException e) {
+                    throw new IllegalArgumentException(
+                            "The specified app process cannot be found." , e);
+                }
             }
 
-            Method method = parseMethodDescriptor(
+            Method method = MethodDescriptorParser.parseMethodDescriptor(
                     getClass().getClassLoader(), methodDescriptor);
             VMDebug.ExecutableMethodFileOffsets location =
                     VMDebug.getExecutableMethodFileOffsets(method);
 
-            if (location == null) {
-                return null;
-            }
-
-            ExecutableMethodFileOffsets ret = new ExecutableMethodFileOffsets();
-            ret.containerPath = location.getContainerPath();
-            ret.containerOffset = location.getContainerOffset();
-            ret.methodOffset = location.getMethodOffset();
-            return ret;
-        }
-    }
-
-    @VisibleForTesting
-    static Method parseMethodDescriptor(ClassLoader classLoader,
-            @NonNull MethodDescriptor descriptor) {
-        try {
-            Class<?> javaClass = classLoader.loadClass(descriptor.fullyQualifiedClassName);
-            Class<?>[] parameters = new Class[descriptor.fullyQualifiedParameters.length];
-            for (int i = 0; i < descriptor.fullyQualifiedParameters.length; i++) {
-                String typeName = descriptor.fullyQualifiedParameters[i];
-                boolean isArrayType = typeName.endsWith("[]");
-                if (isArrayType) {
-                    typeName = typeName.substring(0, typeName.length() - 2);
+            try {
+                if (location == null) {
+                    callback.onResult(null);
+                    return;
                 }
-                switch (typeName) {
-                    case "boolean":
-                        parameters[i] = isArrayType ? boolean.class.arrayType() : boolean.class;
-                        break;
-                    case "byte":
-                        parameters[i] = isArrayType ? byte.class.arrayType() : byte.class;
-                        break;
-                    case "char":
-                        parameters[i] = isArrayType ? char.class.arrayType() : char.class;
-                        break;
-                    case "short":
-                        parameters[i] = isArrayType ? short.class.arrayType() : short.class;
-                        break;
-                    case "int":
-                        parameters[i] = isArrayType ? int.class.arrayType() : int.class;
-                        break;
-                    case "long":
-                        parameters[i] = isArrayType ? long.class.arrayType() : long.class;
-                        break;
-                    case "float":
-                        parameters[i] = isArrayType ? float.class.arrayType() : float.class;
-                        break;
-                    case "double":
-                        parameters[i] = isArrayType ? double.class.arrayType() : double.class;
-                        break;
-                    default:
-                        parameters[i] = isArrayType ? classLoader.loadClass(typeName).arrayType()
-                                : classLoader.loadClass(typeName);
-                }
-            }
 
-            return javaClass.getDeclaredMethod(descriptor.methodName, parameters);
-        } catch (ClassNotFoundException | NoSuchMethodException e) {
-            throw new IllegalArgumentException(
-                    "The specified method cannot be found. Is this descriptor valid? "
-                            + descriptor, e);
+                ExecutableMethodFileOffsets ret = new ExecutableMethodFileOffsets();
+                ret.containerPath = location.getContainerPath();
+                ret.containerOffset = location.getContainerOffset();
+                ret.methodOffset = location.getMethodOffset();
+                callback.onResult(ret);
+            } catch (RemoteException e) {
+                throw new RuntimeException("Failed to invoke result callback", e);
+            }
         }
     }
 }
diff --git a/services/core/java/com/android/server/pm/SaferIntentUtils.java b/services/core/java/com/android/server/pm/SaferIntentUtils.java
index 854e142..ec91da9 100644
--- a/services/core/java/com/android/server/pm/SaferIntentUtils.java
+++ b/services/core/java/com/android/server/pm/SaferIntentUtils.java
@@ -213,6 +213,7 @@
      * CTS tests. The code in this method shall properly avoid control flows using these arguments.
      */
     public static void blockNullAction(IntentArgs args, List componentList) {
+        if (args.intent.getAction() != null) return;
         if (ActivityManager.canAccessUnexportedComponents(args.callingUid)) return;
 
         final Computer computer = (Computer) args.snapshot;
@@ -235,14 +236,11 @@
                 }
                 final ParsedMainComponent comp = infoToComponent(
                         resolveInfo.getComponentInfo(), resolver, args.isReceiver);
-                if (comp != null && !comp.getIntents().isEmpty()
-                        && args.intent.getAction() == null) {
+                if (comp != null && !comp.getIntents().isEmpty()) {
                     match = false;
                 }
             } else if (c instanceof IntentFilter) {
-                if (args.intent.getAction() == null) {
-                    match = false;
-                }
+                match = false;
             }
 
             if (!match) {
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 90d3834..2781592 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -27,6 +27,7 @@
 import static android.app.ActivityManager.START_RETURN_LOCK_TASK_MODE_VIOLATION;
 import static android.app.ActivityManager.START_SUCCESS;
 import static android.app.ActivityManager.START_TASK_TO_FRONT;
+import static android.app.ActivityManager.isStartResultSuccessful;
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
 import static android.app.PendingIntent.FLAG_ONE_SHOT;
@@ -891,7 +892,10 @@
                 final ActivityOptions originalOptions = mRequest.activityOptions != null
                         ? mRequest.activityOptions.getOriginalOptions() : null;
                 // Only track the launch time of activity that will be resumed.
-                launchingRecord = mDoResume ? mLastStartActivityRecord : null;
+                if (mDoResume || (isStartResultSuccessful(res)
+                        && mLastStartActivityRecord.getTask().isVisibleRequested())) {
+                    launchingRecord = mLastStartActivityRecord;
+                }
                 // If the new record is the one that started, a new activity has created.
                 final boolean newActivityCreated = mStartActivity == launchingRecord;
                 // Notify ActivityMetricsLogger that the activity has launched.
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 4ed1206..810aa04 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -4489,7 +4489,7 @@
     }
 
     void onPictureInPictureParamsChanged() {
-        if (inPinnedWindowingMode()) {
+        if (inPinnedWindowingMode() || Flags.enableDesktopWindowingPip()) {
             dispatchTaskInfoChangedIfNeeded(true /* force */);
         }
     }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index b42ce64f..bf4cb45 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9020,16 +9020,19 @@
 
         clearPointerDownOutsideFocusRunnable();
 
+        final InputTarget focusedInputTarget = mFocusedInputTarget;
         if (shouldDelayTouchOutside(t)) {
-            mPointerDownOutsideFocusRunnable = () -> handlePointerDownOutsideFocus(t);
+            mPointerDownOutsideFocusRunnable =
+                    () -> handlePointerDownOutsideFocus(t, focusedInputTarget);
             mH.postDelayed(mPointerDownOutsideFocusRunnable, POINTER_DOWN_OUTSIDE_FOCUS_TIMEOUT_MS);
         } else if (!fromHandler) {
             // Still post the runnable to handler thread in case there is already a runnable
             // in execution, but still waiting to hold the wm lock.
-            mPointerDownOutsideFocusRunnable = () -> handlePointerDownOutsideFocus(t);
+            mPointerDownOutsideFocusRunnable =
+                    () -> handlePointerDownOutsideFocus(t, focusedInputTarget);
             mH.post(mPointerDownOutsideFocusRunnable);
         } else {
-            handlePointerDownOutsideFocus(t);
+            handlePointerDownOutsideFocus(t, focusedInputTarget);
         }
     }
 
@@ -9061,8 +9064,15 @@
         return shouldDelayTouchForEmbeddedActivity || shouldDelayTouchForFreeform;
     }
 
-    private void handlePointerDownOutsideFocus(InputTarget t) {
+    private void handlePointerDownOutsideFocus(InputTarget t, InputTarget focusedInputTarget) {
         synchronized (mGlobalLock) {
+            if (mFocusedInputTarget != focusedInputTarget) {
+                // Skip if the mFocusedInputTarget is already changed. This is possible if the
+                // pointer-down-outside-focus event is delayed to be handled.
+                ProtoLog.i(WM_DEBUG_FOCUS_LIGHT,
+                        "Skip onPointerDownOutsideFocusLocked due to input target changed %s", t);
+                return;
+            }
             if (mPointerDownOutsideFocusRunnable != null
                     && mH.hasCallbacks(mPointerDownOutsideFocusRunnable)) {
                 // Skip if there's another pending pointer-down-outside-focus event.
diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java
index 5492ba6..6e14bad 100644
--- a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java
+++ b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java
@@ -20,6 +20,7 @@
 import static org.junit.Assert.assertThrows;
 
 import android.os.instrumentation.MethodDescriptor;
+import android.os.instrumentation.MethodDescriptorParser;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.filters.SmallTest;
@@ -37,7 +38,7 @@
 
 /**
  * Test class for
- * {@link DynamicInstrumentationManagerService#parseMethodDescriptor(ClassLoader,
+ * {@link MethodDescriptorParser#parseMethodDescriptor(ClassLoader,
  * MethodDescriptor)}.
  * <p>
  * Build/Install/Run:
@@ -119,13 +120,13 @@
     }
 
     private Method parseMethodDescriptor(String fqcn, String methodName) {
-        return DynamicInstrumentationManagerService.parseMethodDescriptor(
+        return MethodDescriptorParser.parseMethodDescriptor(
                 getClass().getClassLoader(),
                 getMethodDescriptor(fqcn, methodName, new String[]{}));
     }
 
     private Method parseMethodDescriptor(String fqcn, String methodName, String[] fqParameters) {
-        return DynamicInstrumentationManagerService.parseMethodDescriptor(
+        return MethodDescriptorParser.parseMethodDescriptor(
                 getClass().getClassLoader(),
                 getMethodDescriptor(fqcn, methodName, fqParameters));
     }
diff --git a/telephony/java/android/telephony/Annotation.java b/telephony/java/android/telephony/Annotation.java
index 8fe107c..09b18b6 100644
--- a/telephony/java/android/telephony/Annotation.java
+++ b/telephony/java/android/telephony/Annotation.java
@@ -109,6 +109,7 @@
             //TelephonyManager.NETWORK_TYPE_LTE_CA,
 
             TelephonyManager.NETWORK_TYPE_NR,
+            TelephonyManager.NETWORK_TYPE_NB_IOT_NTN,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface NetworkType {
diff --git a/telephony/java/android/telephony/RadioAccessFamily.java b/telephony/java/android/telephony/RadioAccessFamily.java
index 90d6f89..8b52f07 100644
--- a/telephony/java/android/telephony/RadioAccessFamily.java
+++ b/telephony/java/android/telephony/RadioAccessFamily.java
@@ -66,6 +66,9 @@
     // 5G
     public static final int RAF_NR = (int) TelephonyManager.NETWORK_TYPE_BITMASK_NR;
 
+    /** NB-IOT (Narrowband Internet of Things) over Non-Terrestrial-Networks technology. */
+    public static final int RAF_NB_IOT_NTN = (int) TelephonyManager.NETWORK_TYPE_BITMASK_NB_IOT_NTN;
+
     // Grouping of RAFs
     // 2G
     private static final int GSM = RAF_GSM | RAF_GPRS | RAF_EDGE;
@@ -80,6 +83,9 @@
     // 5G
     private static final int NR = RAF_NR;
 
+    /** Non-Terrestrial Network. */
+    private static final int NB_IOT_NTN = RAF_NB_IOT_NTN;
+
     /* Phone ID of phone */
     private int mPhoneId;
 
@@ -258,7 +264,7 @@
         raf = ((EVDO & raf) > 0) ? (EVDO | raf) : raf;
         raf = ((LTE & raf) > 0) ? (LTE | raf) : raf;
         raf = ((NR & raf) > 0) ? (NR | raf) : raf;
-
+        raf = ((NB_IOT_NTN & raf) > 0) ? (NB_IOT_NTN | raf) : raf;
         return raf;
     }
 
@@ -364,6 +370,7 @@
             case "WCDMA":   return WCDMA;
             case "LTE_CA":  return RAF_LTE_CA;
             case "NR":      return RAF_NR;
+            case "NB_IOT_NTN": return RAF_NB_IOT_NTN;
             default:        return RAF_UNKNOWN;
         }
     }
diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java
index 127bbff..f8c3287 100644
--- a/telephony/java/android/telephony/ServiceState.java
+++ b/telephony/java/android/telephony/ServiceState.java
@@ -233,6 +233,12 @@
     public static final int  RIL_RADIO_TECHNOLOGY_NR = 20;
 
     /**
+     * 3GPP NB-IOT (Narrowband Internet of Things) over Non-Terrestrial-Networks technology.
+     * @hide
+     */
+    public static final int RIL_RADIO_TECHNOLOGY_NB_IOT_NTN = 21;
+
+    /**
      * RIL Radio Annotation
      * @hide
      */
@@ -258,14 +264,16 @@
         ServiceState.RIL_RADIO_TECHNOLOGY_TD_SCDMA,
         ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN,
         ServiceState.RIL_RADIO_TECHNOLOGY_LTE_CA,
-        ServiceState.RIL_RADIO_TECHNOLOGY_NR})
+        ServiceState.RIL_RADIO_TECHNOLOGY_NR,
+        ServiceState.RIL_RADIO_TECHNOLOGY_NB_IOT_NTN
+    })
     public @interface RilRadioTechnology {}
 
 
     /**
      * The number of the radio technologies.
      */
-    private static final int NEXT_RIL_RADIO_TECHNOLOGY = 21;
+    private static final int NEXT_RIL_RADIO_TECHNOLOGY = 22;
 
     /** @hide */
     public static final int RIL_RADIO_CDMA_TECHNOLOGY_BITMASK =
@@ -1125,6 +1133,9 @@
             case RIL_RADIO_TECHNOLOGY_NR:
                 rtString = "NR_SA";
                 break;
+            case RIL_RADIO_TECHNOLOGY_NB_IOT_NTN:
+                rtString = "NB_IOT_NTN";
+                break;
             default:
                 rtString = "Unexpected";
                 Rlog.w(LOG_TAG, "Unexpected radioTechnology=" + rt);
@@ -1668,6 +1679,8 @@
                 return TelephonyManager.NETWORK_TYPE_LTE_CA;
             case RIL_RADIO_TECHNOLOGY_NR:
                 return TelephonyManager.NETWORK_TYPE_NR;
+            case RIL_RADIO_TECHNOLOGY_NB_IOT_NTN:
+                return TelephonyManager.NETWORK_TYPE_NB_IOT_NTN;
             default:
                 return TelephonyManager.NETWORK_TYPE_UNKNOWN;
         }
@@ -1697,6 +1710,7 @@
                 return AccessNetworkType.CDMA2000;
             case RIL_RADIO_TECHNOLOGY_LTE:
             case RIL_RADIO_TECHNOLOGY_LTE_CA:
+            case RIL_RADIO_TECHNOLOGY_NB_IOT_NTN:
                 return AccessNetworkType.EUTRAN;
             case RIL_RADIO_TECHNOLOGY_NR:
                 return AccessNetworkType.NGRAN;
@@ -1757,6 +1771,8 @@
                 return RIL_RADIO_TECHNOLOGY_LTE_CA;
             case TelephonyManager.NETWORK_TYPE_NR:
                 return RIL_RADIO_TECHNOLOGY_NR;
+            case TelephonyManager.NETWORK_TYPE_NB_IOT_NTN:
+                return RIL_RADIO_TECHNOLOGY_NB_IOT_NTN;
             default:
                 return RIL_RADIO_TECHNOLOGY_UNKNOWN;
         }
@@ -1866,7 +1882,8 @@
                 || radioTechnology == RIL_RADIO_TECHNOLOGY_TD_SCDMA
                 || radioTechnology == RIL_RADIO_TECHNOLOGY_IWLAN
                 || radioTechnology == RIL_RADIO_TECHNOLOGY_LTE_CA
-                || radioTechnology == RIL_RADIO_TECHNOLOGY_NR;
+                || radioTechnology == RIL_RADIO_TECHNOLOGY_NR
+                || radioTechnology == RIL_RADIO_TECHNOLOGY_NB_IOT_NTN;
 
     }
 
@@ -1886,7 +1903,8 @@
     public static boolean isPsOnlyTech(int radioTechnology) {
         return radioTechnology == RIL_RADIO_TECHNOLOGY_LTE
                 || radioTechnology == RIL_RADIO_TECHNOLOGY_LTE_CA
-                || radioTechnology == RIL_RADIO_TECHNOLOGY_NR;
+                || radioTechnology == RIL_RADIO_TECHNOLOGY_NR
+                || radioTechnology == RIL_RADIO_TECHNOLOGY_NB_IOT_NTN;
     }
 
     /** @hide */
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 65a52da..aec11c4 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -3114,6 +3114,12 @@
      * For 5G NSA, the network type will be {@link #NETWORK_TYPE_LTE}.
      */
     public static final int NETWORK_TYPE_NR = TelephonyProtoEnums.NETWORK_TYPE_NR; // 20.
+    /**
+     * 3GPP NB-IOT (Narrowband Internet of Things) over Non-Terrestrial-Networks technology.
+     */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
+    public static final int NETWORK_TYPE_NB_IOT_NTN =
+            TelephonyProtoEnums.NETWORK_TYPE_NB_IOT_NTN; // 21
 
     private static final @NetworkType int[] NETWORK_TYPES = {
             NETWORK_TYPE_GPRS,
@@ -3190,6 +3196,7 @@
      * @see #NETWORK_TYPE_EHRPD
      * @see #NETWORK_TYPE_HSPAP
      * @see #NETWORK_TYPE_NR
+     * @see #NETWORK_TYPE_NB_IOT_NTN
      *
      * @hide
      */
@@ -3250,6 +3257,7 @@
      * @see #NETWORK_TYPE_EHRPD
      * @see #NETWORK_TYPE_HSPAP
      * @see #NETWORK_TYPE_NR
+     * @see #NETWORK_TYPE_NB_IOT_NTN
      *
      * @throws UnsupportedOperationException If the device does not have
      *          {@link PackageManager#FEATURE_TELEPHONY_RADIO_ACCESS}.
@@ -3400,6 +3408,8 @@
                 return "LTE_CA";
             case NETWORK_TYPE_NR:
                 return "NR";
+            case NETWORK_TYPE_NB_IOT_NTN:
+                return "NB_IOT_NTN";
             case NETWORK_TYPE_UNKNOWN:
                 return "UNKNOWN";
             default:
@@ -3450,6 +3460,8 @@
                 return NETWORK_TYPE_BITMASK_LTE;
             case NETWORK_TYPE_NR:
                 return NETWORK_TYPE_BITMASK_NR;
+            case NETWORK_TYPE_NB_IOT_NTN:
+                return NETWORK_TYPE_BITMASK_NB_IOT_NTN;
             case NETWORK_TYPE_IWLAN:
                 return NETWORK_TYPE_BITMASK_IWLAN;
             case NETWORK_TYPE_IDEN:
@@ -10160,6 +10172,9 @@
      * This API will result in allowing an intersection of allowed network types for all reasons,
      * including the configuration done through other reasons.
      *
+     * If device supports satellite service, then
+     * {@link #NETWORK_TYPE_NB_IOT_NTN} is added to allowed network types for reason by default.
+     *
      * @param reason the reason the allowed network type change is taking place
      * @param allowedNetworkTypes The bitmask of allowed network type
      * @throws IllegalStateException if the Telephony process is not currently available.
@@ -10209,6 +10224,10 @@
      * <p>Requires permission: android.Manifest.READ_PRIVILEGED_PHONE_STATE or
      * that the calling app has carrier privileges (see {@link #hasCarrierPrivileges}).
      *
+     * If device supports satellite service, then
+     * {@link #NETWORK_TYPE_NB_IOT_NTN} is added to allowed network types for reason by
+     * default.
+     *
      * @param reason the reason the allowed network type change is taking place
      * @return the allowed network type bitmask
      * @throws IllegalStateException    if the Telephony process is not currently available.
@@ -10275,7 +10294,7 @@
      */
     public static String convertNetworkTypeBitmaskToString(
             @NetworkTypeBitMask long networkTypeBitmask) {
-        String networkTypeName = IntStream.rangeClosed(NETWORK_TYPE_GPRS, NETWORK_TYPE_NR)
+        String networkTypeName = IntStream.rangeClosed(NETWORK_TYPE_GPRS, NETWORK_TYPE_NB_IOT_NTN)
                 .filter(x -> {
                     return (networkTypeBitmask & getBitMaskForNetworkType(x))
                             == getBitMaskForNetworkType(x);
@@ -14905,7 +14924,8 @@
                     NETWORK_TYPE_BITMASK_LTE_CA,
                     NETWORK_TYPE_BITMASK_NR,
                     NETWORK_TYPE_BITMASK_IWLAN,
-                    NETWORK_TYPE_BITMASK_IDEN
+                    NETWORK_TYPE_BITMASK_IDEN,
+                    NETWORK_TYPE_BITMASK_NB_IOT_NTN
             })
     public @interface NetworkTypeBitMask {}
 
@@ -15006,6 +15026,12 @@
      */
     public static final long NETWORK_TYPE_BITMASK_IWLAN = (1 << (NETWORK_TYPE_IWLAN -1));
 
+    /**
+     * network type bitmask indicating the support of readio tech NB IOT NTN.
+     */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
+    public static final long NETWORK_TYPE_BITMASK_NB_IOT_NTN = (1 << (NETWORK_TYPE_NB_IOT_NTN - 1));
+
     /** @hide */
     public static final long NETWORK_CLASS_BITMASK_2G = NETWORK_TYPE_BITMASK_GSM
                 | NETWORK_TYPE_BITMASK_GPRS
@@ -15034,6 +15060,9 @@
     public static final long NETWORK_CLASS_BITMASK_5G = NETWORK_TYPE_BITMASK_NR;
 
     /** @hide */
+    public static final long NETWORK_CLASS_BITMASK_NTN = NETWORK_TYPE_BITMASK_NB_IOT_NTN;
+
+    /** @hide */
     public static final long NETWORK_STANDARDS_FAMILY_BITMASK_3GPP = NETWORK_TYPE_BITMASK_GSM
             | NETWORK_TYPE_BITMASK_GPRS
             | NETWORK_TYPE_BITMASK_EDGE
@@ -15045,7 +15074,8 @@
             | NETWORK_TYPE_BITMASK_TD_SCDMA
             | NETWORK_TYPE_BITMASK_LTE
             | NETWORK_TYPE_BITMASK_LTE_CA
-            | NETWORK_TYPE_BITMASK_NR;
+            | NETWORK_TYPE_BITMASK_NR
+            | NETWORK_TYPE_BITMASK_NB_IOT_NTN;
 
     /** @hide */
     public static final long NETWORK_STANDARDS_FAMILY_BITMASK_3GPP2 = NETWORK_TYPE_BITMASK_CDMA
@@ -18083,7 +18113,7 @@
      */
     public static boolean isNetworkTypeValid(@NetworkType int networkType) {
         return networkType >= TelephonyManager.NETWORK_TYPE_UNKNOWN &&
-                networkType <= TelephonyManager.NETWORK_TYPE_NR;
+                networkType <= TelephonyManager.NETWORK_TYPE_NB_IOT_NTN;
     }
 
     /**