Merge "Improve handling of launching translucent occluding activities." into main
diff --git a/api/gen_combined_removed_dex.sh b/api/gen_combined_removed_dex.sh
index e0153f7..2860e2e 100755
--- a/api/gen_combined_removed_dex.sh
+++ b/api/gen_combined_removed_dex.sh
@@ -6,6 +6,6 @@
 
 # Convert each removed.txt to the "dex format" equivalent, and print all output.
 for f in "$@"; do
-    "$metalava_path" signature-to-dex "$f" "${tmp_dir}/tmp"
+    "$metalava_path" signature-to-dex "$f" --out "${tmp_dir}/tmp"
     cat "${tmp_dir}/tmp"
 done
diff --git a/core/api/current.txt b/core/api/current.txt
index 44a6c6b..5456c15 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -43901,6 +43901,7 @@
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT = "satellite_connection_hysteresis_sec_int";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT = "satellite_entitlement_status_refresh_days_int";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL = "satellite_entitlement_supported_bool";
+    field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_ESOS_SUPPORTED_BOOL = "satellite_esos_supported_bool";
     field public static final String KEY_SHOW_4G_FOR_3G_DATA_ICON_BOOL = "show_4g_for_3g_data_icon_bool";
     field public static final String KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL = "show_4g_for_lte_data_icon_bool";
     field public static final String KEY_SHOW_APN_SETTING_CDMA_BOOL = "show_apn_setting_cdma_bool";
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 0ab2588..e4a8407 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -318,6 +318,14 @@
 
 }
 
+package android.net.wifi {
+
+  public final class WifiMigration {
+    method @FlaggedApi("android.net.wifi.flags.legacy_keystore_to_wifi_blobstore_migration") public static void migrateLegacyKeystoreToWifiBlobstore();
+  }
+
+}
+
 package android.nfc {
 
   public class NfcServiceManager {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index b3c471c..4536f6f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3447,6 +3447,7 @@
   }
 
   public static interface VirtualDeviceManager.ActivityListener {
+    method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public default void onActivityLaunchBlocked(int, @NonNull android.content.ComponentName, int);
     method public void onDisplayEmpty(int);
     method @Deprecated public void onTopActivityChanged(int, @NonNull android.content.ComponentName);
     method public default void onTopActivityChanged(int, @NonNull android.content.ComponentName, int);
@@ -11399,8 +11400,9 @@
     method public long getNumPacketsTx();
     method public long getRxTimeMillis();
     method public long getSleepTimeMillis();
-    method @NonNull public long getTimeInRatMicros(int);
-    method @NonNull public long getTimeInRxSignalStrengthLevelMicros(@IntRange(from=android.telephony.CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN, to=android.telephony.CellSignalStrength.SIGNAL_STRENGTH_GREAT) int);
+    method public long getTimeInRatMicros(int);
+    method public long getTimeInRxSignalStrengthLevelMicros(@IntRange(from=android.telephony.CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN, to=android.telephony.CellSignalStrength.SIGNAL_STRENGTH_GREAT) int);
+    method @FlaggedApi("com.android.server.power.optimization.streamlined_connectivity_battery_stats") public long getTxTimeMillis(@IntRange(from=android.telephony.ModemActivityInfo.TX_POWER_LEVEL_0, to=android.telephony.ModemActivityInfo.TX_POWER_LEVEL_4) int);
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.os.connectivity.CellularBatteryStats> CREATOR;
   }
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 1352465..b7de93a 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2608,6 +2608,18 @@
 
 }
 
+package android.os.connectivity {
+
+  public final class CellularBatteryStats implements android.os.Parcelable {
+    ctor @FlaggedApi("com.android.server.power.optimization.streamlined_connectivity_battery_stats") public CellularBatteryStats(long, long, long, long, long, long, long, long, long, long, @NonNull long[], @NonNull long[], @NonNull long[], long);
+  }
+
+  public final class WifiBatteryStats implements android.os.Parcelable {
+    ctor @FlaggedApi("com.android.server.power.optimization.streamlined_connectivity_battery_stats") public WifiBatteryStats(long, long, long, long, long, long, long, long, long, long, long, long, long, @NonNull long[], @NonNull long[], @NonNull long[], long);
+  }
+
+}
+
 package android.os.health {
 
   public class HealthKeys {
diff --git a/core/java/Android.bp b/core/java/Android.bp
index fae411d..128fb62 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -20,10 +20,43 @@
         "**/*.java",
         "**/*.aidl",
         ":framework-nfc-non-updatable-sources",
+        ":messagequeue-gen",
+    ],
+    // Exactly one of the below will be added to srcs by messagequeue-gen
+    exclude_srcs: [
+        "android/os/LegacyMessageQueue/MessageQueue.java",
+        "android/os/ConcurrentMessageQueue/MessageQueue.java",
+        "android/os/SemiConcurrentMessageQueue/MessageQueue.java",
     ],
     visibility: ["//frameworks/base"],
 }
 
+// Add selected MessageQueue.java implementation to srcs
+soong_config_module_type {
+    name: "release_package_messagequeue_implementation_srcs",
+    module_type: "genrule",
+    config_namespace: "messagequeue",
+    value_variables: ["release_package_messagequeue_implementation"],
+    properties: [
+        "srcs",
+    ],
+}
+
+// Output the selected android/os/MessageQueue.java implementation
+release_package_messagequeue_implementation_srcs {
+    name: "messagequeue-gen",
+    soong_config_variables: {
+        release_package_messagequeue_implementation: {
+            srcs: ["android/os/%s"],
+            conditions_default: {
+                srcs: ["android/os/LegacyMessageQueue/MessageQueue.java"],
+            },
+        },
+    },
+    cmd: "mkdir -p android/os/; cp $(in) $(out);",
+    out: ["android/os/MessageQueue.java"],
+}
+
 aidl_library {
     name: "IDropBoxManagerService_aidl",
     srcs: [
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 2313fa2..5214d2c 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -2981,9 +2981,7 @@
         new AppOpInfo.Builder(OP_ESTABLISH_VPN_MANAGER, OPSTR_ESTABLISH_VPN_MANAGER,
                 "ESTABLISH_VPN_MANAGER").setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
         new AppOpInfo.Builder(OP_ACCESS_RESTRICTED_SETTINGS, OPSTR_ACCESS_RESTRICTED_SETTINGS,
-                "ACCESS_RESTRICTED_SETTINGS").setDefaultMode(
-                        android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()
-                                ? MODE_DEFAULT : MODE_ALLOWED)
+                "ACCESS_RESTRICTED_SETTINGS").setDefaultMode(AppOpsManager.MODE_ALLOWED)
             .setDisableReset(true).setRestrictRead(true).build(),
         new AppOpInfo.Builder(OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO,
                 "RECEIVE_SOUNDTRIGGER_AUDIO").setDefaultMode(AppOpsManager.MODE_ALLOWED)
diff --git a/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl b/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl
index fc7f85c..39371a3 100644
--- a/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl
+++ b/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl
@@ -32,7 +32,7 @@
      * @param topActivity The component name of the top activity.
      * @param userId The user ID associated with the top activity.
      */
-    void onTopActivityChanged(int displayId, in ComponentName topActivity, in int userId);
+    void onTopActivityChanged(int displayId, in ComponentName topActivity, int userId);
 
     /**
      * Called when the display becomes empty (e.g. if the user hits back on the last
@@ -41,4 +41,13 @@
      * @param displayId The display ID that became empty.
      */
     void onDisplayEmpty(int displayId);
+
+    /**
+     * Called when an activity launch was blocked due to a policy violation.
+     *
+     * @param displayId The display ID on which the activity tried to launch.
+     * @param componentName The component name of the blocked activity.
+     * @param userId The user ID associated with the blocked activity.
+     */
+    void onActivityLaunchBlocked(int displayId, in ComponentName componentName, int userId);
 }
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index 4cbcb68..d3fcfc6 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -128,6 +128,22 @@
                         Binder.restoreCallingIdentity(token);
                     }
                 }
+
+                @Override
+                public void onActivityLaunchBlocked(int displayId, ComponentName componentName,
+                        @UserIdInt int userId) {
+                    final long token = Binder.clearCallingIdentity();
+                    try {
+                        synchronized (mActivityListenersLock) {
+                            for (int i = 0; i < mActivityListeners.size(); i++) {
+                                mActivityListeners.valueAt(i)
+                                        .onActivityLaunchBlocked(displayId, componentName, userId);
+                            }
+                        }
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                }
             };
     private final IVirtualDeviceSoundEffectListener mSoundEffectListener =
             new IVirtualDeviceSoundEffectListener.Stub() {
@@ -525,6 +541,12 @@
         public void onDisplayEmpty(int displayId) {
             mExecutor.execute(() -> mActivityListener.onDisplayEmpty(displayId));
         }
+
+        public void onActivityLaunchBlocked(int displayId, ComponentName componentName,
+                @UserIdInt int userId) {
+            mExecutor.execute(() ->
+                    mActivityListener.onActivityLaunchBlocked(displayId, componentName, userId));
+        }
     }
 
     /**
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 88c3d38..296ca33 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -1126,7 +1126,7 @@
     }
 
     /**
-     * Listener for activity changes in this virtual device.
+     * Listener for activity changes and other activity events on a virtual device.
      *
      * @hide
      */
@@ -1167,6 +1167,20 @@
          * @param displayId The display ID that became empty.
          */
         void onDisplayEmpty(int displayId);
+
+        /**
+         * Called when an activity launch was blocked due to a policy violation.
+         *
+         * @param displayId The display ID on which the activity tried to launch.
+         * @param componentName The component name of the blocked activity.
+         * @param userId The user ID associated with the blocked activity.
+         *
+         * @see VirtualDeviceParams#POLICY_TYPE_ACTIVITY
+         * @see VirtualDevice#addActivityPolicyExemption(ComponentName)
+         */
+        @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+        default void onActivityLaunchBlocked(int displayId, @NonNull ComponentName componentName,
+                @UserIdInt int userId) {}
     }
 
     /**
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index cd8082c..64d2081 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -41,6 +41,13 @@
 
 flag {
      namespace: "virtual_devices"
+     name: "activity_control_api"
+     description: "Enable APIs for fine grained activity policy, fallback and callbacks"
+     bug: "333443509"
+}
+
+flag {
+     namespace: "virtual_devices"
      name: "camera_device_awareness"
      description: "Enable device awareness in camera service"
      bug: "305170199"
diff --git a/core/java/android/hardware/devicestate/feature/flags.aconfig b/core/java/android/hardware/devicestate/feature/flags.aconfig
index 12d3f94..a09c84d 100644
--- a/core/java/android/hardware/devicestate/feature/flags.aconfig
+++ b/core/java/android/hardware/devicestate/feature/flags.aconfig
@@ -8,4 +8,13 @@
     description: "Updated DeviceState hasProperty API"
     bug: "293636629"
     is_fixed_read_only: true
+}
+
+flag {
+    name: "device_state_property_migration"
+    is_exported: true
+    namespace: "windowing_sdk"
+    description: "Client migration to updated DeviceStateManager API's"
+    bug: "336640888"
+    is_fixed_read_only: true
 }
\ No newline at end of file
diff --git a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
new file mode 100644
index 0000000..72b5cf7
--- /dev/null
+++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
@@ -0,0 +1,1648 @@
+/*
+ * 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 android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Handler;
+import android.os.Trace;
+import android.util.Log;
+import android.util.Printer;
+import android.util.SparseArray;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.FileDescriptor;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Low-level class holding the list of messages to be dispatched by a
+ * {@link Looper}.  Messages are not added directly to a MessageQueue,
+ * but rather through {@link Handler} objects associated with the Looper.
+ *
+ * <p>You can retrieve the MessageQueue for the current thread with
+ * {@link Looper#myQueue() Looper.myQueue()}.
+ */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
+@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
+        "com.android.platform.test.ravenwood.nativesubstitution.MessageQueue_host")
+public final class MessageQueue {
+    private static final String TAG = "ConcurrentMessageQueue";
+    private static final boolean DEBUG = false;
+    private static final boolean TRACE = false;
+
+    // True if the message queue can be quit.
+    private final boolean mQuitAllowed;
+
+    @SuppressWarnings("unused")
+    private long mPtr; // used by native code
+
+    @IntDef(value = {
+        STACK_NODE_MESSAGE,
+        STACK_NODE_ACTIVE,
+        STACK_NODE_PARKED,
+        STACK_NODE_TIMEDPARK})
+    @Retention(RetentionPolicy.SOURCE)
+    private @interface StackNodeType {}
+
+    /*
+     * Stack node types. STACK_NODE_MESSAGE indicates a node containing a message.
+     * The other types indicate what state our Looper thread is in. The bottom of
+     * the stack is always a single state node. Message nodes are added on top.
+     */
+    private static final int STACK_NODE_MESSAGE = 0;
+    /*
+     * Active state indicates that next() is processing messages
+     */
+    private static final int STACK_NODE_ACTIVE = 1;
+    /*
+     * Parked state indicates that the Looper thread is sleeping indefinitely (nothing to deliver)
+     */
+    private static final int STACK_NODE_PARKED = 2;
+    /*
+     * Timed Park state indicates that the Looper thread is sleeping, waiting for a message
+     * deadline
+     */
+    private static final int STACK_NODE_TIMEDPARK = 3;
+
+    /* Describes a node in the Treiber stack */
+    static class StackNode {
+        @StackNodeType
+        private final int mType;
+
+        StackNode(@StackNodeType int type) {
+            mType = type;
+        }
+
+        @StackNodeType
+        final int getNodeType() {
+            return mType;
+        }
+
+        final boolean isMessageNode() {
+            return mType == STACK_NODE_MESSAGE;
+        }
+    }
+
+    static final class MessageNode extends StackNode implements Comparable<MessageNode> {
+        private final Message mMessage;
+        volatile StackNode mNext;
+        StateNode mBottomOfStack;
+        boolean mWokeUp;
+        final long mInsertSeq;
+        private static final VarHandle sRemovedFromStack;
+        private volatile boolean mRemovedFromStackValue;
+        static {
+            try {
+                MethodHandles.Lookup l = MethodHandles.lookup();
+                sRemovedFromStack = l.findVarHandle(MessageQueue.MessageNode.class,
+                        "mRemovedFromStackValue", boolean.class);
+            } catch (Exception e) {
+                Log.wtf(TAG, "VarHandle lookup failed with exception: " + e);
+                throw new ExceptionInInitializerError(e);
+            }
+        }
+
+        MessageNode(@NonNull Message message, long insertSeq) {
+            super(STACK_NODE_MESSAGE);
+            mMessage = message;
+            mInsertSeq = insertSeq;
+        }
+
+        long getWhen() {
+            return mMessage.when;
+        }
+
+        boolean isRemovedFromStack() {
+            return mRemovedFromStackValue;
+        }
+
+        boolean removeFromStack() {
+            return sRemovedFromStack.compareAndSet(this, false, true);
+        }
+
+        boolean isAsync() {
+            return mMessage.isAsynchronous();
+        }
+
+        boolean isBarrier() {
+            return mMessage.target == null;
+        }
+
+        @Override
+        public int compareTo(@NonNull MessageNode messageNode) {
+            Message other = messageNode.mMessage;
+
+            int compared = Long.compare(mMessage.when, other.when);
+            if (compared == 0) {
+                compared = Long.compare(mInsertSeq, messageNode.mInsertSeq);
+            }
+            return compared;
+        }
+    }
+
+    static class StateNode extends StackNode {
+        StateNode(int type) {
+            super(type);
+        }
+    }
+
+    static final class TimedParkStateNode extends StateNode {
+        long mWhenToWake;
+
+        TimedParkStateNode() {
+            super(STACK_NODE_TIMEDPARK);
+        }
+    }
+
+    private static final StateNode sStackStateActive = new StateNode(STACK_NODE_ACTIVE);
+    private static final StateNode sStackStateParked = new StateNode(STACK_NODE_PARKED);
+    private final TimedParkStateNode mStackStateTimedPark = new TimedParkStateNode();
+
+    /* This is the top of our treiber stack. */
+    private static final VarHandle sState;
+    static {
+        try {
+            MethodHandles.Lookup l = MethodHandles.lookup();
+            sState = l.findVarHandle(MessageQueue.class, "mStateValue",
+                    MessageQueue.StackNode.class);
+        } catch (Exception e) {
+            Log.wtf(TAG, "VarHandle lookup failed with exception: " + e);
+            throw new ExceptionInInitializerError(e);
+        }
+    }
+
+    private volatile StackNode mStateValue = sStackStateParked;
+    private final ConcurrentSkipListSet<MessageNode> mPriorityQueue =
+            new ConcurrentSkipListSet<MessageNode>();
+    private final ConcurrentSkipListSet<MessageNode> mAsyncPriorityQueue =
+            new ConcurrentSkipListSet<MessageNode>();
+
+    /*
+     * This helps us ensure that messages with the same timestamp are inserted in FIFO order.
+     * Increments on each insert, starting at 0. MessageNode.compareTo() will compare sequences
+     * when delivery timestamps are identical.
+     */
+    private static final VarHandle sNextInsertSeq;
+    private volatile long mNextInsertSeqValue = 0;
+    /*
+     * The exception to the FIFO order rule is sendMessageAtFrontOfQueue().
+     * Those messages must be in LIFO order - SIGH.
+     * Decrements on each front of queue insert.
+     */
+    private static final VarHandle sNextFrontInsertSeq;
+    private volatile long mNextFrontInsertSeqValue = -1;
+    static {
+        try {
+            MethodHandles.Lookup l = MethodHandles.lookup();
+            sNextInsertSeq = l.findVarHandle(MessageQueue.class, "mNextInsertSeqValue",
+                    long.class);
+            sNextFrontInsertSeq = l.findVarHandle(MessageQueue.class, "mNextFrontInsertSeqValue",
+                    long.class);
+        } catch (Exception e) {
+            Log.wtf(TAG, "VarHandle lookup failed with exception: " + e);
+            throw new ExceptionInInitializerError(e);
+        }
+
+    }
+
+    /*
+     * Tracks the number of queued and cancelled messages in our stack.
+     *
+     * On item cancellation, determine whether to wake next() to flush tombstoned messages.
+     * We track queued and cancelled counts as two ints packed into a single long.
+     */
+    private static final class MessageCounts {
+        private static VarHandle sCounts;
+        private volatile long mCountsValue = 0;
+        static {
+            try {
+                MethodHandles.Lookup l = MethodHandles.lookup();
+                sCounts = l.findVarHandle(MessageQueue.MessageCounts.class, "mCountsValue",
+                        long.class);
+            } catch (Exception e) {
+                Log.wtf(TAG, "VarHandle lookup failed with exception: " + e);
+                throw new ExceptionInInitializerError(e);
+            }
+        }
+
+        /* We use a special value to indicate when next() has been woken for flush. */
+        private static final long AWAKE = Long.MAX_VALUE;
+        /*
+         * Minimum number of messages in the stack which we need before we consider flushing
+         * tombstoned items.
+         */
+        private static final int MESSAGE_FLUSH_THRESHOLD = 10;
+
+        private static int numQueued(long val) {
+            return (int) (val >>> Integer.SIZE);
+        }
+
+        private static int numCancelled(long val) {
+            return (int) val;
+        }
+
+        private static long combineCounts(int queued, int cancelled) {
+            return ((long) queued << Integer.SIZE) | (long) cancelled;
+        }
+
+        public void incrementQueued() {
+            while (true) {
+                long oldVal = mCountsValue;
+                int queued = numQueued(oldVal);
+                int cancelled = numCancelled(oldVal);
+                /* Use Math.max() to avoid overflow of queued count */
+                long newVal = combineCounts(Math.max(queued + 1, queued), cancelled);
+
+                /* Don't overwrite 'AWAKE' state */
+                if (oldVal == AWAKE || sCounts.compareAndSet(this, oldVal, newVal)) {
+                    break;
+                }
+            }
+        }
+
+        public boolean incrementCancelled() {
+            while (true) {
+                long oldVal = mCountsValue;
+                if (oldVal == AWAKE) {
+                    return false;
+                }
+                int queued = numQueued(oldVal);
+                int cancelled = numCancelled(oldVal);
+                boolean needsPurge = queued > MESSAGE_FLUSH_THRESHOLD
+                        && (queued >> 1) < cancelled;
+                long newVal;
+                if (needsPurge) {
+                    newVal = AWAKE;
+                } else {
+                    newVal = combineCounts(queued,
+                            Math.max(cancelled + 1, cancelled));
+                }
+
+                if (sCounts.compareAndSet(this, oldVal, newVal)) {
+                    return needsPurge;
+                }
+            }
+        }
+
+        public void clearCounts() {
+            mCountsValue = 0;
+        }
+    }
+
+    private final MessageCounts mMessageCounts = new MessageCounts();
+
+    private final Object mIdleHandlersLock = new Object();
+    @GuardedBy("mIdleHandlersLock")
+    private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
+    private IdleHandler[] mPendingIdleHandlers;
+
+    private final Object mFileDescriptorRecordsLock = new Object();
+    @GuardedBy("mFileDescriptorRecordsLock")
+    private SparseArray<FileDescriptorRecord> mFileDescriptorRecords;
+
+    private static final VarHandle sQuitting;
+    private boolean mQuittingValue = false;
+    static {
+        try {
+            MethodHandles.Lookup l = MethodHandles.lookup();
+            sQuitting = l.findVarHandle(MessageQueue.class, "mQuittingValue", boolean.class);
+        } catch (Exception e) {
+            Log.wtf(TAG, "VarHandle lookup failed with exception: " + e);
+            throw new ExceptionInInitializerError(e);
+        }
+    }
+
+    // The next barrier token.
+    // Barriers are indicated by messages with a null target whose arg1 field carries the token.
+    private final AtomicInteger mNextBarrierToken = new AtomicInteger(1);
+
+    private static native long nativeInit();
+    private static native void nativeDestroy(long ptr);
+    private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
+    private static native void nativeWake(long ptr);
+    private static native boolean nativeIsPolling(long ptr);
+    private static native void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
+
+    MessageQueue(boolean quitAllowed) {
+        mQuitAllowed = quitAllowed;
+        mPtr = nativeInit();
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            dispose();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    // Disposes of the underlying message queue.
+    // Must only be called on the looper thread or the finalizer.
+    private void dispose() {
+        if (mPtr != 0) {
+            nativeDestroy(mPtr);
+            mPtr = 0;
+        }
+    }
+
+    /**
+     * Returns true if the looper has no pending messages which are due to be processed.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @return True if the looper is idle.
+     */
+    public boolean isIdle() {
+        MessageNode msgNode = null;
+        MessageNode asyncMsgNode = null;
+
+        if (!mPriorityQueue.isEmpty()) {
+            try {
+                msgNode = mPriorityQueue.first();
+            } catch (NoSuchElementException e) { }
+        }
+
+        if (!mAsyncPriorityQueue.isEmpty()) {
+            try {
+                asyncMsgNode = mAsyncPriorityQueue.first();
+            } catch (NoSuchElementException e) { }
+        }
+
+        final long now = SystemClock.uptimeMillis();
+        if ((msgNode != null && msgNode.getWhen() <= now)
+                || (asyncMsgNode != null && asyncMsgNode.getWhen() <= now)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /* Protects mNextIsDrainingStack */
+    private final ReentrantLock mDrainingLock = new ReentrantLock();
+    private boolean mNextIsDrainingStack = false;
+    private final Condition mDrainCompleted = mDrainingLock.newCondition();
+
+    /**
+     * Add a new {@link IdleHandler} to this message queue.  This may be
+     * removed automatically for you by returning false from
+     * {@link IdleHandler#queueIdle IdleHandler.queueIdle()} when it is
+     * invoked, or explicitly removing it with {@link #removeIdleHandler}.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @param handler The IdleHandler to be added.
+     */
+    public void addIdleHandler(@NonNull IdleHandler handler) {
+        if (handler == null) {
+            throw new NullPointerException("Can't add a null IdleHandler");
+        }
+        synchronized (mIdleHandlersLock) {
+            mIdleHandlers.add(handler);
+        }
+    }
+
+    /**
+     * Remove an {@link IdleHandler} from the queue that was previously added
+     * with {@link #addIdleHandler}.  If the given object is not currently
+     * in the idle list, nothing is done.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @param handler The IdleHandler to be removed.
+     */
+    public void removeIdleHandler(@NonNull IdleHandler handler) {
+        synchronized (mIdleHandlersLock) {
+            mIdleHandlers.remove(handler);
+        }
+    }
+
+    /**
+     * Returns whether this looper's thread is currently polling for more work to do.
+     * This is a good signal that the loop is still alive rather than being stuck
+     * handling a callback.  Note that this method is intrinsically racy, since the
+     * state of the loop can change before you get the result back.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @return True if the looper is currently polling for events.
+     * @hide
+     */
+    public boolean isPolling() {
+        // If the loop is quitting then it must not be idling.
+        // We can assume mPtr != 0 when sQuitting is false.
+        return !((boolean) sQuitting.getVolatile(this)) && nativeIsPolling(mPtr);
+    }
+
+    /* Helper to choose the correct queue to insert into. */
+    private void insertIntoPriorityQueue(MessageNode msgNode) {
+        if (msgNode.isAsync()) {
+            mAsyncPriorityQueue.add(msgNode);
+        } else {
+            mPriorityQueue.add(msgNode);
+        }
+    }
+
+    private boolean removeFromPriorityQueue(MessageNode msgNode) {
+        if (msgNode.isAsync()) {
+            return mAsyncPriorityQueue.remove(msgNode);
+        } else {
+            return mPriorityQueue.remove(msgNode);
+        }
+    }
+
+    private MessageNode pickEarliestNode(MessageNode nodeA, MessageNode nodeB) {
+        if (nodeA != null && nodeB != null) {
+            if (nodeA.compareTo(nodeB) < 0) {
+                return nodeA;
+            }
+            return nodeB;
+        }
+
+        return nodeA != null ? nodeA : nodeB;
+    }
+
+    private MessageNode iterateNext(Iterator<MessageNode> iter) {
+        if (iter.hasNext()) {
+            try {
+                return iter.next();
+            } catch (NoSuchElementException e) {
+                /* The queue is empty - this can happen if we race with remove */
+            }
+        }
+        return null;
+    }
+
+    /* Move any non-cancelled messages into the priority queue */
+    private void drainStack(StackNode oldTop) {
+        while (oldTop.isMessageNode()) {
+            MessageNode oldTopMessageNode = (MessageNode) oldTop;
+            if (oldTopMessageNode.removeFromStack()) {
+                insertIntoPriorityQueue(oldTopMessageNode);
+            }
+            MessageNode inserted = oldTopMessageNode;
+            oldTop = oldTopMessageNode.mNext;
+            /*
+             * removeMessages can walk this list while we are consuming it.
+             * Set our next pointer to null *after* we add the message to our
+             * priority queue. This way removeMessages() will always find the
+             * message, either in our list or in the priority queue.
+             */
+            inserted.mNext = null;
+        }
+    }
+
+    /* Set the stack state to Active, return a list of nodes to walk. */
+    private StackNode swapAndSetStackStateActive() {
+        while (true) {
+            /* Set stack state to Active, get node list to walk later */
+            StackNode current = (StackNode) sState.getVolatile(this);
+            if (current == sStackStateActive
+                    || sState.compareAndSet(this, current, sStackStateActive)) {
+                return current;
+            }
+        }
+    }
+
+    /* This is only read/written from the Looper thread */
+    private int mNextPollTimeoutMillis;
+    private static final AtomicLong mMessagesDelivered = new AtomicLong();
+
+    private Message nextMessage() {
+        int i = 0;
+
+        while (true) {
+            if (DEBUG) {
+                Log.d(TAG, "nextMessage loop #" + i);
+                i++;
+            }
+
+            mDrainingLock.lock();
+            mNextIsDrainingStack = true;
+            mDrainingLock.unlock();
+
+            /*
+             * Set our state to active, drain any items from the stack into our priority queues
+             */
+            StackNode oldTop;
+            oldTop = swapAndSetStackStateActive();
+            drainStack(oldTop);
+
+            mDrainingLock.lock();
+            mNextIsDrainingStack = false;
+            mDrainCompleted.signalAll();
+            mDrainingLock.unlock();
+
+            /*
+             * The objective of this next block of code is to:
+             *  - find a message to return (if any is ready)
+             *  - find a next message we would like to return, after scheduling.
+             *     - we make our scheduling decision based on this next message (if it exists).
+             *
+             * We have two queues to juggle and the presence of barriers throws an additional
+             * wrench into our plans.
+             *
+             * The last wrinkle is that remove() may delete items from underneath us. If we hit
+             * that case, we simply restart the loop.
+             */
+
+            /* Get the first node from each queue */
+            Iterator<MessageNode> queueIter = mPriorityQueue.iterator();
+            MessageNode msgNode = iterateNext(queueIter);
+            Iterator<MessageNode> asyncQueueIter = mAsyncPriorityQueue.iterator();
+            MessageNode asyncMsgNode = iterateNext(asyncQueueIter);
+
+            if (DEBUG) {
+                if (msgNode != null) {
+                    Message msg = msgNode.mMessage;
+                    Log.d(TAG, "Next found node what: " + msg.what + " when: " + msg.when
+                            + " seq: " + msgNode.mInsertSeq + "barrier: "
+                            + msgNode.isBarrier() + " now: " + SystemClock.uptimeMillis());
+                }
+                if (asyncMsgNode != null) {
+                    Message msg = asyncMsgNode.mMessage;
+                    Log.d(TAG, "Next found async node what: " + msg.what + " when: " + msg.when
+                            + " seq: " + asyncMsgNode.mInsertSeq + "barrier: "
+                            + asyncMsgNode.isBarrier() + " now: "
+                            + SystemClock.uptimeMillis());
+                }
+            }
+
+            /*
+             * the node which we will return, null if none are ready
+             */
+            MessageNode found = null;
+            /*
+             * The node from which we will determine our next wakeup time.
+             * Null indicates there is no next message ready. If we found a node,
+             * we can leave this null as Looper will call us again after delivering
+             * the message.
+             */
+            MessageNode next = null;
+
+            long now = SystemClock.uptimeMillis();
+            /*
+             * If we have a barrier we should return the async node (if it exists and is ready)
+             */
+            if (msgNode != null && msgNode.isBarrier()) {
+                if (asyncMsgNode != null && now >= asyncMsgNode.getWhen()) {
+                    found = asyncMsgNode;
+                } else {
+                    next = asyncMsgNode;
+                }
+            } else { /* No barrier. */
+                MessageNode earliest;
+                /*
+                 * If we have two messages, pick the earliest option from either queue.
+                 * Otherwise grab whichever node is non-null. If both are null we'll fall through.
+                 */
+                earliest = pickEarliestNode(msgNode, asyncMsgNode);
+
+                if (earliest != null) {
+                    if (now >= earliest.getWhen()) {
+                        found = earliest;
+                    } else {
+                        next = earliest;
+                    }
+                }
+            }
+
+            if (DEBUG) {
+                if (found != null) {
+                    Message msg = found.mMessage;
+                    Log.d(TAG, "Will deliver node what: " + msg.what + " when: " + msg.when
+                            + " seq: " + found.mInsertSeq + " barrier: " + found.isBarrier()
+                            + " async: " + found.isAsync() + " now: "
+                            + SystemClock.uptimeMillis());
+                } else {
+                    Log.d(TAG, "No node to deliver");
+                }
+                if (next != null) {
+                    Message msg = next.mMessage;
+                    Log.d(TAG, "Next node what: " + msg.what + " when: " + msg.when + " seq: "
+                            + next.mInsertSeq + " barrier: " + next.isBarrier() + " async: "
+                            + next.isAsync()
+                            + " now: " + SystemClock.uptimeMillis());
+                } else {
+                    Log.d(TAG, "No next node");
+                }
+            }
+
+            /*
+             * If we have a found message, we will get called again so there's no need to set state.
+             * In that case we can leave our state as ACTIVE.
+             *
+             * Otherwise we should determine how to park the thread.
+             */
+            StateNode nextOp = sStackStateActive;
+            if (found == null) {
+                if (next == null) {
+                    /* No message to deliver, sleep indefinitely */
+                    mNextPollTimeoutMillis = -1;
+                    nextOp = sStackStateParked;
+                    if (DEBUG) {
+                        Log.d(TAG, "nextMessage next state is StackStateParked");
+                    }
+                } else {
+                    /* Message not ready, or we found one to deliver already, set a timeout */
+                    long nextMessageWhen = next.getWhen();
+                    if (nextMessageWhen > now) {
+                        mNextPollTimeoutMillis = (int) Math.min(nextMessageWhen - now,
+                                Integer.MAX_VALUE);
+                    } else {
+                        mNextPollTimeoutMillis = 0;
+                    }
+
+                    mStackStateTimedPark.mWhenToWake = now + mNextPollTimeoutMillis;
+                    nextOp = mStackStateTimedPark;
+                    if (DEBUG) {
+                        Log.d(TAG, "nextMessage next state is StackStateTimedParked timeout ms "
+                                + mNextPollTimeoutMillis + " mWhenToWake: "
+                                + mStackStateTimedPark.mWhenToWake + " now " + now);
+                    }
+                }
+            }
+
+            /*
+             * Try to swap our state from Active back to Park or TimedPark. If we raced with
+             * enqueue, loop back around to pick up any new items.
+             */
+            if (sState.compareAndSet(this, sStackStateActive, nextOp)) {
+                mMessageCounts.clearCounts();
+                if (found != null) {
+                    if (!removeFromPriorityQueue(found)) {
+                        /*
+                         * RemoveMessages() might be able to pull messages out from under us
+                         * However we can detect that here and just loop around if it happens.
+                         */
+                        continue;
+                    }
+
+                    if (TRACE) {
+                        Trace.setCounter("MQ.Delivered", mMessagesDelivered.incrementAndGet());
+                    }
+                    return found.mMessage;
+                }
+                return null;
+            }
+        }
+    }
+
+    Message next() {
+        final long ptr = mPtr;
+        if (ptr == 0) {
+            return null;
+        }
+
+        mNextPollTimeoutMillis = 0;
+        int pendingIdleHandlerCount = -1; // -1 only during first iteration
+        while (true) {
+            if (mNextPollTimeoutMillis != 0) {
+                Binder.flushPendingCommands();
+            }
+
+            nativePollOnce(ptr, mNextPollTimeoutMillis);
+
+            Message msg = nextMessage();
+            if (msg != null) {
+                msg.markInUse();
+                return msg;
+            }
+
+            if ((boolean) sQuitting.getVolatile(this)) {
+                return null;
+            }
+
+            synchronized (mIdleHandlersLock) {
+                // If first time idle, then get the number of idlers to run.
+                // Idle handles only run if the queue is empty or if the first message
+                // in the queue (possibly a barrier) is due to be handled in the future.
+                if (pendingIdleHandlerCount < 0
+                        && mNextPollTimeoutMillis != 0) {
+                    pendingIdleHandlerCount = mIdleHandlers.size();
+                }
+                if (pendingIdleHandlerCount <= 0) {
+                    // No idle handlers to run.  Loop and wait some more.
+                    continue;
+                }
+
+                if (mPendingIdleHandlers == null) {
+                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
+                }
+                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
+            }
+
+            // Run the idle handlers.
+            // We only ever reach this code block during the first iteration.
+            for (int i = 0; i < pendingIdleHandlerCount; i++) {
+                final IdleHandler idler = mPendingIdleHandlers[i];
+                mPendingIdleHandlers[i] = null; // release the reference to the handler
+
+                boolean keep = false;
+                try {
+                    keep = idler.queueIdle();
+                } catch (Throwable t) {
+                    Log.wtf(TAG, "IdleHandler threw exception", t);
+                }
+
+                if (!keep) {
+                    synchronized (mIdleHandlersLock) {
+                        mIdleHandlers.remove(idler);
+                    }
+                }
+            }
+
+            // Reset the idle handler count to 0 so we do not run them again.
+            pendingIdleHandlerCount = 0;
+
+            // While calling an idle handler, a new message could have been delivered
+            // so go back and look again for a pending message without waiting.
+            mNextPollTimeoutMillis = 0;
+        }
+    }
+
+    void quit(boolean safe) {
+        if (!mQuitAllowed) {
+            throw new IllegalStateException("Main thread not allowed to quit.");
+        }
+        synchronized (mIdleHandlersLock) {
+            if (sQuitting.compareAndSet(this, false, true)) {
+                if (safe) {
+                    removeAllFutureMessages();
+                } else {
+                    removeAllMessages();
+                }
+
+                // We can assume mPtr != 0 because sQuitting was previously false.
+                nativeWake(mPtr);
+            }
+        }
+    }
+
+    boolean enqueueMessage(@NonNull Message msg, long when) {
+        if (msg.target == null) {
+            throw new IllegalArgumentException("Message must have a target.");
+        }
+
+        if (msg.isInUse()) {
+            throw new IllegalStateException(msg + " This message is already in use.");
+        }
+
+        return enqueueMessageUnchecked(msg, when);
+    }
+
+    private boolean enqueueMessageUnchecked(@NonNull Message msg, long when) {
+        if ((boolean) sQuitting.getVolatile(this)) {
+            IllegalStateException e = new IllegalStateException(
+                    msg.target + " sending message to a Handler on a dead thread");
+            Log.w(TAG, e.getMessage(), e);
+            msg.recycleUnchecked();
+            return false;
+        }
+
+        long seq = when != 0 ? ((long)sNextInsertSeq.getAndAdd(this, 1L) + 1L)
+                : ((long)sNextFrontInsertSeq.getAndAdd(this, -1L) - 1L);
+        /* TODO: Add a MessageNode member to Message so we can avoid this allocation */
+        MessageNode node = new MessageNode(msg, seq);
+        msg.when = when;
+        msg.markInUse();
+
+        if (DEBUG) {
+            Log.d(TAG, "Insert message what: " + msg.what + " when: " + msg.when + " seq: "
+                    + node.mInsertSeq + " barrier: " + node.isBarrier() + " async: "
+                    + node.isAsync() + " now: " + SystemClock.uptimeMillis());
+        }
+
+        while (true) {
+            StackNode old = (StackNode) sState.getVolatile(this);
+            boolean wakeNeeded;
+            boolean inactive;
+
+            node.mNext = old;
+            switch (old.getNodeType()) {
+                case STACK_NODE_ACTIVE:
+                    /*
+                     * The worker thread is currently active and will process any elements added to
+                     * the stack before parking again.
+                     */
+                    node.mBottomOfStack = (StateNode) old;
+                    inactive = false;
+                    node.mWokeUp = true;
+                    wakeNeeded = false;
+                    break;
+
+                case STACK_NODE_PARKED:
+                    node.mBottomOfStack = (StateNode) old;
+                    inactive = true;
+                    node.mWokeUp = true;
+                    wakeNeeded = true;
+                    break;
+
+                case STACK_NODE_TIMEDPARK:
+                    node.mBottomOfStack = (StateNode) old;
+                    inactive = true;
+                    wakeNeeded = mStackStateTimedPark.mWhenToWake >= node.getWhen();
+                    node.mWokeUp = wakeNeeded;
+                    break;
+
+                default:
+                    MessageNode oldMessage = (MessageNode) old;
+
+                    node.mBottomOfStack = oldMessage.mBottomOfStack;
+                    int bottomType = node.mBottomOfStack.getNodeType();
+                    inactive = bottomType >= STACK_NODE_PARKED;
+                    wakeNeeded = (bottomType == STACK_NODE_TIMEDPARK
+                            && mStackStateTimedPark.mWhenToWake >= node.getWhen()
+                            && !oldMessage.mWokeUp);
+                    node.mWokeUp = oldMessage.mWokeUp || wakeNeeded;
+                    break;
+            }
+            if (sState.compareAndSet(this, old, node)) {
+                if (inactive) {
+                    if (wakeNeeded) {
+                        nativeWake(mPtr);
+                    } else {
+                        mMessageCounts.incrementQueued();
+                    }
+                }
+                return true;
+            }
+        }
+    }
+
+    /**
+     * Posts a synchronization barrier to the Looper's message queue.
+     *
+     * Message processing occurs as usual until the message queue encounters the
+     * synchronization barrier that has been posted.  When the barrier is encountered,
+     * later synchronous messages in the queue are stalled (prevented from being executed)
+     * until the barrier is released by calling {@link #removeSyncBarrier} and specifying
+     * the token that identifies the synchronization barrier.
+     *
+     * This method is used to immediately postpone execution of all subsequently posted
+     * synchronous messages until a condition is met that releases the barrier.
+     * Asynchronous messages (see {@link Message#isAsynchronous} are exempt from the barrier
+     * and continue to be processed as usual.
+     *
+     * This call must be always matched by a call to {@link #removeSyncBarrier} with
+     * the same token to ensure that the message queue resumes normal operation.
+     * Otherwise the application will probably hang!
+     *
+     * @return A token that uniquely identifies the barrier.  This token must be
+     * passed to {@link #removeSyncBarrier} to release the barrier.
+     *
+     * @hide
+     */
+    @TestApi
+    public int postSyncBarrier() {
+        return postSyncBarrier(SystemClock.uptimeMillis());
+    }
+
+    private int postSyncBarrier(long when) {
+        final int token = mNextBarrierToken.getAndIncrement();
+        final Message msg = Message.obtain();
+
+        msg.markInUse();
+        msg.arg1 = token;
+
+        if (!enqueueMessageUnchecked(msg, when)) {
+            Log.wtf(TAG, "Unexpected error while adding sync barrier!");
+            return -1;
+        }
+
+        return token;
+    }
+
+    private class MatchBarrierToken extends MessageCompare {
+        int mBarrierToken;
+
+        MatchBarrierToken(int token) {
+            super();
+            mBarrierToken = token;
+        }
+
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == null && m.arg1 == mBarrierToken) {
+                return true;
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Removes a synchronization barrier.
+     *
+     * @param token The synchronization barrier token that was returned by
+     * {@link #postSyncBarrier}.
+     *
+     * @throws IllegalStateException if the barrier was not found.
+     *
+     * @hide
+     */
+    @TestApi
+    public void removeSyncBarrier(int token) {
+        boolean removed;
+        MessageNode first;
+        final MatchBarrierToken matchBarrierToken = new MatchBarrierToken(token);
+
+        try {
+            /* Retain the first element to see if we are currently stuck on a barrier. */
+            first = mPriorityQueue.first();
+        } catch (NoSuchElementException e) {
+            /* The queue is empty */
+            first = null;
+        }
+
+        removed = findOrRemoveMessages(null, 0, null, null, 0, matchBarrierToken, true);
+        if (removed && first != null) {
+            Message m = first.mMessage;
+            if (m.target == null && m.arg1 == token) {
+                /* Wake up next() in case it was sleeping on this barrier. */
+                nativeWake(mPtr);
+            }
+        } else if (!removed) {
+            throw new IllegalStateException("The specified message queue synchronization "
+                    + " barrier token has not been posted or has already been removed.");
+        }
+    }
+
+    private StateNode getStateNode(StackNode node) {
+        if (node.isMessageNode()) {
+            return ((MessageNode) node).mBottomOfStack;
+        }
+        return (StateNode) node;
+    }
+
+    private void waitForDrainCompleted() {
+        mDrainingLock.lock();
+        while (mNextIsDrainingStack) {
+            mDrainCompleted.awaitUninterruptibly();
+        }
+        mDrainingLock.unlock();
+    }
+
+    /*
+     * This class is used to find matches for hasMessages() and removeMessages()
+     */
+    private abstract static class MessageCompare {
+        public abstract boolean compareMessage(Message m, Handler h, int what, Object object,
+                Runnable r, long when);
+    }
+
+    private boolean stackHasMessages(Handler h, int what, Object object, Runnable r, long when,
+            MessageCompare compare, boolean removeMatches) {
+        boolean found = false;
+        StackNode top = (StackNode) sState.getVolatile(this);
+        StateNode bottom = getStateNode(top);
+
+        /*
+         * If the top node is a state node, there are no reachable messages.
+         * If it's anything other than Active, we can quit as we know that next() is not
+         * consuming items.
+         * If the top node is Active then we know that next() is currently consuming items.
+         * In that case we should wait next() has drained the stack.
+         */
+        if (top == bottom) {
+            if (bottom != sStackStateActive) {
+                return false;
+            }
+            waitForDrainCompleted();
+            return false;
+        }
+
+        /*
+         * We have messages that we may tombstone. Walk the stack until we hit the bottom or we
+         * hit a null pointer.
+         * If we hit the bottom, we are done.
+         * If we hit a null pointer, then the stack is being consumed by next() and we must cycle
+         * until the stack has been drained.
+         */
+        MessageNode p = (MessageNode) top;
+
+        while (true) {
+            if (compare.compareMessage(p.mMessage, h, what, object, r, when)) {
+                found = true;
+                if (DEBUG) {
+                    Log.w(TAG, "stackHasMessages node matches");
+                }
+                if (removeMatches) {
+                    if (p.removeFromStack()) {
+                        p.mMessage.recycleUnchecked();
+                        if (mMessageCounts.incrementCancelled()) {
+                            nativeWake(mPtr);
+                        }
+                    }
+                } else {
+                    return true;
+                }
+            }
+
+            StackNode n = p.mNext;
+            if (n == null) {
+                /* Next() is walking the stack, we must re-sample */
+                if (DEBUG) {
+                    Log.d(TAG, "stackHasMessages next() is walking the stack, we must re-sample");
+                }
+                waitForDrainCompleted();
+                break;
+            }
+            if (!n.isMessageNode()) {
+                /* We reached the end of the stack */
+                return found;
+            }
+            p = (MessageNode) n;
+        }
+
+        return found;
+    }
+
+    private boolean priorityQueueHasMessage(ConcurrentSkipListSet<MessageNode> queue, Handler h,
+            int what, Object object, Runnable r, long when, MessageCompare compare,
+            boolean removeMatches) {
+        Iterator<MessageNode> iterator = queue.iterator();
+        boolean found = false;
+
+        while (iterator.hasNext()) {
+            MessageNode msg = iterator.next();
+
+            if (compare.compareMessage(msg.mMessage, h, what, object, r, when)) {
+                if (removeMatches) {
+                    found = true;
+                    if (queue.remove(msg)) {
+                        msg.mMessage.recycleUnchecked();
+                    }
+                } else {
+                    return true;
+                }
+            }
+        }
+        return found;
+    }
+
+    private boolean findOrRemoveMessages(Handler h, int what, Object object, Runnable r, long when,
+            MessageCompare compare, boolean removeMatches) {
+        boolean foundInStack, foundInQueue;
+
+        foundInStack = stackHasMessages(h, what, object, r, when, compare, removeMatches);
+        foundInQueue = priorityQueueHasMessage(mPriorityQueue, h, what, object, r, when, compare,
+                removeMatches);
+        foundInQueue |= priorityQueueHasMessage(mAsyncPriorityQueue, h, what, object, r, when,
+                compare, removeMatches);
+
+        return foundInStack || foundInQueue;
+    }
+
+    private static class MatchHandlerWhatAndObject extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.what == what && (object == null || m.obj == object)) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerWhatAndObject mMatchHandlerWhatAndObject =
+            new MatchHandlerWhatAndObject();
+    boolean hasMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return false;
+        }
+
+        return findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObject, false);
+    }
+
+    private static class MatchHandlerWhatAndObjectEquals extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.what == what && (object == null || object.equals(m.obj))) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerWhatAndObjectEquals mMatchHandlerWhatAndObjectEquals =
+            new MatchHandlerWhatAndObjectEquals();
+    boolean hasEqualMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return false;
+        }
+
+        return findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObjectEquals,
+                false);
+    }
+
+    private static class MatchHandlerRunnableAndObject extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.callback == r && (object == null || m.obj == object)) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerRunnableAndObject mMatchHandlerRunnableAndObject =
+            new MatchHandlerRunnableAndObject();
+
+    boolean hasMessages(Handler h, Runnable r, Object object) {
+        if (h == null) {
+            return false;
+        }
+
+        return findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject, false);
+    }
+
+    private static class MatchHandler extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandler mMatchHandler = new MatchHandler();
+    boolean hasMessages(Handler h) {
+        if (h == null) {
+            return false;
+        }
+        return findOrRemoveMessages(h, -1, null, null, 0, mMatchHandler, false);
+    }
+
+    void removeMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return;
+        }
+        findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObject, true);
+    }
+
+    void removeEqualMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return;
+        }
+        findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObjectEquals, true);
+    }
+
+    void removeMessages(Handler h, Runnable r, Object object) {
+        if (h == null || r == null) {
+            return;
+        }
+        findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject, true);
+    }
+
+    private static class MatchHandlerRunnableAndObjectEquals extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.callback == r && (object == null || object.equals(m.obj))) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerRunnableAndObjectEquals mMatchHandlerRunnableAndObjectEquals =
+            new MatchHandlerRunnableAndObjectEquals();
+    void removeEqualMessages(Handler h, Runnable r, Object object) {
+        if (h == null || r == null) {
+            return;
+        }
+        findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObjectEquals, true);
+    }
+
+    private static class MatchHandlerAndObject extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && (object == null || m.obj == object)) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerAndObject mMatchHandlerAndObject = new MatchHandlerAndObject();
+    void removeCallbacksAndMessages(Handler h, Object object) {
+        if (h == null) {
+            return;
+        }
+        findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObject, true);
+    }
+
+    private static class MatchHandlerAndObjectEquals extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && (object == null || object.equals(m.obj))) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerAndObjectEquals mMatchHandlerAndObjectEquals =
+            new MatchHandlerAndObjectEquals();
+    void removeCallbacksAndEqualMessages(Handler h, Object object) {
+        if (h == null) {
+            return;
+        }
+        findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObjectEquals, true);
+    }
+
+    private static class MatchAllMessages extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            return true;
+        }
+    }
+    private final MatchAllMessages mMatchAllMessages = new MatchAllMessages();
+    private void removeAllMessages() {
+        findOrRemoveMessages(null, -1, null, null, 0, mMatchAllMessages, true);
+    }
+
+    private static class MatchAllFutureMessages extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.when > when) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchAllFutureMessages mMatchAllFutureMessages = new MatchAllFutureMessages();
+    private void removeAllFutureMessages() {
+        findOrRemoveMessages(null, -1, null, null, SystemClock.uptimeMillis(),
+                mMatchAllFutureMessages, true);
+    }
+
+    private void printPriorityQueueNodes() {
+        Iterator<MessageNode> iterator = mPriorityQueue.iterator();
+
+        Log.d(TAG, "* Dump priority queue");
+        while (iterator.hasNext()) {
+            MessageNode msgNode = iterator.next();
+            Log.d(TAG, "** MessageNode what: " + msgNode.mMessage.what + " when "
+                    + msgNode.mMessage.when + " seq: " + msgNode.mInsertSeq);
+        }
+    }
+
+    private int dumpPriorityQueue(ConcurrentSkipListSet<MessageNode> queue, Printer pw,
+            String prefix, Handler h, int n) {
+        int count = 0;
+        long now = SystemClock.uptimeMillis();
+
+        for (MessageNode msgNode : queue) {
+            Message msg = msgNode.mMessage;
+            if (h == null || h == msg.target) {
+                pw.println(prefix + "Message " + (n + count) + ": " + msg.toString(now));
+            }
+            count++;
+        }
+        return count;
+    }
+
+    void dump(Printer pw, String prefix, Handler h) {
+        long now = SystemClock.uptimeMillis();
+        int n = 0;
+
+        pw.println(prefix + "(MessageQueue is using Concurrent implementation)");
+
+        StackNode node = (StackNode) sState.getVolatile(this);
+        while (node != null) {
+            if (node.isMessageNode()) {
+                Message msg = ((MessageNode) node).mMessage;
+                if (h == null || h == msg.target) {
+                    pw.println(prefix + "Message " + n + ": " + msg.toString(now));
+                }
+                node = ((MessageNode) node).mNext;
+            } else {
+                pw.println(prefix + "State: " + node);
+                node = null;
+            }
+            n++;
+        }
+
+        pw.println(prefix + "PriorityQueue Messages: ");
+        n += dumpPriorityQueue(mPriorityQueue, pw, prefix, h, n);
+        pw.println(prefix + "AsyncPriorityQueue Messages: ");
+        n += dumpPriorityQueue(mAsyncPriorityQueue, pw, prefix, h, n);
+
+        pw.println(prefix + "(Total messages: " + n + ", polling=" + isPolling()
+                + ", quitting=" + (boolean) sQuitting.getVolatile(this) + ")");
+    }
+
+    private int dumpPriorityQueue(ConcurrentSkipListSet<MessageNode> queue,
+            ProtoOutputStream proto) {
+        int count = 0;
+
+        for (MessageNode msgNode : queue) {
+            Message msg = msgNode.mMessage;
+            msg.dumpDebug(proto, MessageQueueProto.MESSAGES);
+            count++;
+        }
+        return count;
+    }
+
+    void dumpDebug(ProtoOutputStream proto, long fieldId) {
+        final long messageQueueToken = proto.start(fieldId);
+
+        StackNode node = (StackNode) sState.getVolatile(this);
+        while (node.isMessageNode()) {
+            Message msg = ((MessageNode) node).mMessage;
+            msg.dumpDebug(proto, MessageQueueProto.MESSAGES);
+            node = ((MessageNode) node).mNext;
+        }
+
+        dumpPriorityQueue(mPriorityQueue, proto);
+        dumpPriorityQueue(mAsyncPriorityQueue, proto);
+
+        proto.write(MessageQueueProto.IS_POLLING_LOCKED, isPolling());
+        proto.write(MessageQueueProto.IS_QUITTING, (boolean) sQuitting.getVolatile(this));
+        proto.end(messageQueueToken);
+    }
+
+    /**
+     * Adds a file descriptor listener to receive notification when file descriptor
+     * related events occur.
+     * <p>
+     * If the file descriptor has already been registered, the specified events
+     * and listener will replace any that were previously associated with it.
+     * It is not possible to set more than one listener per file descriptor.
+     * </p><p>
+     * It is important to always unregister the listener when the file descriptor
+     * is no longer of use.
+     * </p>
+     *
+     * @param fd The file descriptor for which a listener will be registered.
+     * @param events The set of events to receive: a combination of the
+     * {@link OnFileDescriptorEventListener#EVENT_INPUT},
+     * {@link OnFileDescriptorEventListener#EVENT_OUTPUT}, and
+     * {@link OnFileDescriptorEventListener#EVENT_ERROR} event masks.  If the requested
+     * set of events is zero, then the listener is unregistered.
+     * @param listener The listener to invoke when file descriptor events occur.
+     *
+     * @see OnFileDescriptorEventListener
+     * @see #removeOnFileDescriptorEventListener
+     */
+    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class)
+    public void addOnFileDescriptorEventListener(@NonNull FileDescriptor fd,
+            @OnFileDescriptorEventListener.Events int events,
+            @NonNull OnFileDescriptorEventListener listener) {
+        if (fd == null) {
+            throw new IllegalArgumentException("fd must not be null");
+        }
+        if (listener == null) {
+            throw new IllegalArgumentException("listener must not be null");
+        }
+
+        synchronized (mFileDescriptorRecordsLock) {
+            updateOnFileDescriptorEventListenerLocked(fd, events, listener);
+        }
+    }
+
+    /**
+     * Removes a file descriptor listener.
+     * <p>
+     * This method does nothing if no listener has been registered for the
+     * specified file descriptor.
+     * </p>
+     *
+     * @param fd The file descriptor whose listener will be unregistered.
+     *
+     * @see OnFileDescriptorEventListener
+     * @see #addOnFileDescriptorEventListener
+     */
+    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class)
+    public void removeOnFileDescriptorEventListener(@NonNull FileDescriptor fd) {
+        if (fd == null) {
+            throw new IllegalArgumentException("fd must not be null");
+        }
+
+        synchronized (mFileDescriptorRecordsLock) {
+            updateOnFileDescriptorEventListenerLocked(fd, 0, null);
+        }
+    }
+
+    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class)
+    private void updateOnFileDescriptorEventListenerLocked(FileDescriptor fd, int events,
+            OnFileDescriptorEventListener listener) {
+        final int fdNum = fd.getInt$();
+
+        int index = -1;
+        FileDescriptorRecord record = null;
+        if (mFileDescriptorRecords != null) {
+            index = mFileDescriptorRecords.indexOfKey(fdNum);
+            if (index >= 0) {
+                record = mFileDescriptorRecords.valueAt(index);
+                if (record != null && record.mEvents == events) {
+                    return;
+                }
+            }
+        }
+
+        if (events != 0) {
+            events |= OnFileDescriptorEventListener.EVENT_ERROR;
+            if (record == null) {
+                if (mFileDescriptorRecords == null) {
+                    mFileDescriptorRecords = new SparseArray<FileDescriptorRecord>();
+                }
+                record = new FileDescriptorRecord(fd, events, listener);
+                mFileDescriptorRecords.put(fdNum, record);
+            } else {
+                record.mListener = listener;
+                record.mEvents = events;
+                record.mSeq += 1;
+            }
+            nativeSetFileDescriptorEvents(mPtr, fdNum, events);
+        } else if (record != null) {
+            record.mEvents = 0;
+            mFileDescriptorRecords.removeAt(index);
+            nativeSetFileDescriptorEvents(mPtr, fdNum, 0);
+        }
+    }
+
+    // Called from native code.
+    private int dispatchEvents(int fd, int events) {
+        // Get the file descriptor record and any state that might change.
+        final FileDescriptorRecord record;
+        final int oldWatchedEvents;
+        final OnFileDescriptorEventListener listener;
+        final int seq;
+        synchronized (mFileDescriptorRecordsLock) {
+            record = mFileDescriptorRecords.get(fd);
+            if (record == null) {
+                return 0; // spurious, no listener registered
+            }
+
+            oldWatchedEvents = record.mEvents;
+            events &= oldWatchedEvents; // filter events based on current watched set
+            if (events == 0) {
+                return oldWatchedEvents; // spurious, watched events changed
+            }
+
+            listener = record.mListener;
+            seq = record.mSeq;
+        }
+
+        // Invoke the listener outside of the lock.
+        int newWatchedEvents = listener.onFileDescriptorEvents(
+                record.mDescriptor, events);
+        if (newWatchedEvents != 0) {
+            newWatchedEvents |= OnFileDescriptorEventListener.EVENT_ERROR;
+        }
+
+        // Update the file descriptor record if the listener changed the set of
+        // events to watch and the listener itself hasn't been updated since.
+        if (newWatchedEvents != oldWatchedEvents) {
+            synchronized (mFileDescriptorRecordsLock) {
+                int index = mFileDescriptorRecords.indexOfKey(fd);
+                if (index >= 0 && mFileDescriptorRecords.valueAt(index) == record
+                        && record.mSeq == seq) {
+                    record.mEvents = newWatchedEvents;
+                    if (newWatchedEvents == 0) {
+                        mFileDescriptorRecords.removeAt(index);
+                    }
+                }
+            }
+        }
+
+        // Return the new set of events to watch for native code to take care of.
+        return newWatchedEvents;
+    }
+
+    /**
+     * Callback interface for discovering when a thread is going to block
+     * waiting for more messages.
+     */
+    public static interface IdleHandler {
+        /**
+         * Called when the message queue has run out of messages and will now
+         * wait for more.  Return true to keep your idle handler active, false
+         * to have it removed.  This may be called if there are still messages
+         * pending in the queue, but they are all scheduled to be dispatched
+         * after the current time.
+         */
+        boolean queueIdle();
+    }
+
+    /**
+     * A listener which is invoked when file descriptor related events occur.
+     */
+    public interface OnFileDescriptorEventListener {
+        /**
+         * File descriptor event: Indicates that the file descriptor is ready for input
+         * operations, such as reading.
+         * <p>
+         * The listener should read all available data from the file descriptor
+         * then return <code>true</code> to keep the listener active or <code>false</code>
+         * to remove the listener.
+         * </p><p>
+         * In the case of a socket, this event may be generated to indicate
+         * that there is at least one incoming connection that the listener
+         * should accept.
+         * </p><p>
+         * This event will only be generated if the {@link #EVENT_INPUT} event mask was
+         * specified when the listener was added.
+         * </p>
+         */
+        public static final int EVENT_INPUT = 1 << 0;
+
+        /**
+         * File descriptor event: Indicates that the file descriptor is ready for output
+         * operations, such as writing.
+         * <p>
+         * The listener should write as much data as it needs.  If it could not
+         * write everything at once, then it should return <code>true</code> to
+         * keep the listener active.  Otherwise, it should return <code>false</code>
+         * to remove the listener then re-register it later when it needs to write
+         * something else.
+         * </p><p>
+         * This event will only be generated if the {@link #EVENT_OUTPUT} event mask was
+         * specified when the listener was added.
+         * </p>
+         */
+        public static final int EVENT_OUTPUT = 1 << 1;
+
+        /**
+         * File descriptor event: Indicates that the file descriptor encountered a
+         * fatal error.
+         * <p>
+         * File descriptor errors can occur for various reasons.  One common error
+         * is when the remote peer of a socket or pipe closes its end of the connection.
+         * </p><p>
+         * This event may be generated at any time regardless of whether the
+         * {@link #EVENT_ERROR} event mask was specified when the listener was added.
+         * </p>
+         */
+        public static final int EVENT_ERROR = 1 << 2;
+
+        /** @hide */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(flag = true, prefix = { "EVENT_" }, value = {
+                EVENT_INPUT,
+                EVENT_OUTPUT,
+                EVENT_ERROR
+        })
+        public @interface Events {}
+
+        /**
+         * Called when a file descriptor receives events.
+         *
+         * @param fd The file descriptor.
+         * @param events The set of events that occurred: a combination of the
+         * {@link #EVENT_INPUT}, {@link #EVENT_OUTPUT}, and {@link #EVENT_ERROR} event masks.
+         * @return The new set of events to watch, or 0 to unregister the listener.
+         *
+         * @see #EVENT_INPUT
+         * @see #EVENT_OUTPUT
+         * @see #EVENT_ERROR
+         */
+        @Events int onFileDescriptorEvents(@NonNull FileDescriptor fd, @Events int events);
+    }
+
+    static final class FileDescriptorRecord {
+        public final FileDescriptor mDescriptor;
+        public int mEvents;
+        public OnFileDescriptorEventListener mListener;
+        public int mSeq;
+
+        public FileDescriptorRecord(FileDescriptor descriptor,
+                int events, OnFileDescriptorEventListener listener) {
+            mDescriptor = descriptor;
+            mEvents = events;
+            mListener = listener;
+        }
+    }
+}
diff --git a/core/java/android/os/MessageQueue.java b/core/java/android/os/LegacyMessageQueue/MessageQueue.java
similarity index 97%
rename from core/java/android/os/MessageQueue.java
rename to core/java/android/os/LegacyMessageQueue/MessageQueue.java
index 5b711c9..6b9b349 100644
--- a/core/java/android/os/MessageQueue.java
+++ b/core/java/android/os/LegacyMessageQueue/MessageQueue.java
@@ -20,6 +20,9 @@
 import android.annotation.NonNull;
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Handler;
+import android.os.Process;
+import android.os.Trace;
 import android.util.Log;
 import android.util.Printer;
 import android.util.SparseArray;
@@ -29,6 +32,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicLong;
 
 /**
  * Low-level class holding the list of messages to be dispatched by a
@@ -44,6 +48,7 @@
 public final class MessageQueue {
     private static final String TAG = "MessageQueue";
     private static final boolean DEBUG = false;
+    private static final boolean TRACE = false;
 
     // True if the message queue can be quit.
     @UnsupportedAppUsage
@@ -326,6 +331,8 @@
         return newWatchedEvents;
     }
 
+    private static final AtomicLong mMessagesDelivered = new AtomicLong();
+
     @UnsupportedAppUsage
     Message next() {
         // Return here if the message loop has already quit and been disposed.
@@ -381,6 +388,9 @@
                         if (msg.isAsynchronous()) {
                             mAsyncMessageCount--;
                         }
+                        if (TRACE) {
+                            Trace.setCounter("MQ.Delivered", mMessagesDelivered.incrementAndGet());
+                        }
                         return msg;
                     }
                 } else {
@@ -794,7 +804,7 @@
                 Message n = p.next;
                 if (n != null) {
                     if (n.target == h && n.what == what
-                        && (object == null || n.obj == object)) {
+                            && (object == null || n.obj == object)) {
                         Message nn = n.next;
                         if (n.isAsynchronous()) {
                             mAsyncMessageCount--;
@@ -841,7 +851,7 @@
                 Message n = p.next;
                 if (n != null) {
                     if (n.target == h && n.what == what
-                        && (object == null || object.equals(n.obj))) {
+                            && (object == null || object.equals(n.obj))) {
                         Message nn = n.next;
                         if (n.isAsynchronous()) {
                             mAsyncMessageCount--;
@@ -888,7 +898,7 @@
                 Message n = p.next;
                 if (n != null) {
                     if (n.target == h && n.callback == r
-                        && (object == null || n.obj == object)) {
+                            && (object == null || n.obj == object)) {
                         Message nn = n.next;
                         if (n.isAsynchronous()) {
                             mAsyncMessageCount--;
@@ -935,7 +945,7 @@
                 Message n = p.next;
                 if (n != null) {
                     if (n.target == h && n.callback == r
-                        && (object == null || object.equals(n.obj))) {
+                            && (object == null || object.equals(n.obj))) {
                         Message nn = n.next;
                         if (n.isAsynchronous()) {
                             mAsyncMessageCount--;
@@ -1093,6 +1103,7 @@
 
     void dump(Printer pw, String prefix, Handler h) {
         synchronized (this) {
+            pw.println(prefix + "(MessageQueue is using Legacy implementation)");
             long now = SystemClock.uptimeMillis();
             int n = 0;
             for (Message msg = mMessages; msg != null; msg = msg.next) {
diff --git a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
new file mode 100644
index 0000000..967332f
--- /dev/null
+++ b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
@@ -0,0 +1,1589 @@
+/*
+ * 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 android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Handler;
+import android.os.Trace;
+import android.util.Log;
+import android.util.Printer;
+import android.util.SparseArray;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.FileDescriptor;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.PriorityQueue;
+import java.util.PriorityQueue;
+import java.util.PriorityQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Low-level class holding the list of messages to be dispatched by a
+ * {@link Looper}.  Messages are not added directly to a MessageQueue,
+ * but rather through {@link Handler} objects associated with the Looper.
+ *
+ * <p>You can retrieve the MessageQueue for the current thread with
+ * {@link Looper#myQueue() Looper.myQueue()}.
+ */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
+@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
+        "com.android.platform.test.ravenwood.nativesubstitution.MessageQueue_host")
+public final class MessageQueue {
+    private static final String TAG = "SemiConcurrentMessageQueue";
+    private static final boolean DEBUG = false;
+    private static final boolean TRACE = false;
+
+    // True if the message queue can be quit.
+    private final boolean mQuitAllowed;
+
+    @SuppressWarnings("unused")
+    private long mPtr; // used by native code
+
+    @IntDef(value = {
+        STACK_NODE_MESSAGE,
+        STACK_NODE_ACTIVE,
+        STACK_NODE_PARKED,
+        STACK_NODE_TIMEDPARK})
+    @Retention(RetentionPolicy.SOURCE)
+    private @interface StackNodeType {}
+
+    /*
+     * Stack node types. STACK_NODE_MESSAGE indicates a node containing a message.
+     * The other types indicate what state our Looper thread is in. The bottom of
+     * the stack is always a single state node. Message nodes are added on top.
+     */
+    private static final int STACK_NODE_MESSAGE = 0;
+    /*
+     * Active state indicates that next() is processing messages
+     */
+    private static final int STACK_NODE_ACTIVE = 1;
+    /*
+     * Parked state indicates that the Looper thread is sleeping indefinitely (nothing to deliver)
+     */
+    private static final int STACK_NODE_PARKED = 2;
+    /*
+     * Timed Park state indicates that the Looper thread is sleeping, waiting for a message
+     * deadline
+     */
+    private static final int STACK_NODE_TIMEDPARK = 3;
+
+    /* Describes a node in the Treiber stack */
+    static class StackNode {
+        @StackNodeType
+        private final int mType;
+
+        StackNode(@StackNodeType int type) {
+            mType = type;
+        }
+
+        @StackNodeType
+        final int getNodeType() {
+            return mType;
+        }
+
+        final boolean isMessageNode() {
+            return mType == STACK_NODE_MESSAGE;
+        }
+    }
+
+    static final class MessageNode extends StackNode implements Comparable<MessageNode> {
+        private final Message mMessage;
+        volatile StackNode mNext;
+        StateNode mBottomOfStack;
+        boolean mWokeUp;
+        boolean mRemovedFromStack = false;
+        final long mInsertSeq;
+
+        MessageNode(@NonNull Message message, long insertSeq) {
+            super(STACK_NODE_MESSAGE);
+            mMessage = message;
+            mInsertSeq = insertSeq;
+        }
+
+        long getWhen() {
+            return mMessage.when;
+        }
+
+        boolean isRemovedFromStack() {
+            return mRemovedFromStack;
+        }
+
+        boolean removeFromStack() {
+            if (!mRemovedFromStack) {
+                mRemovedFromStack = true;
+                return true;
+            }
+            return false;
+        }
+
+        boolean isAsync() {
+            return mMessage.isAsynchronous();
+        }
+
+        boolean isBarrier() {
+            return mMessage.target == null;
+        }
+
+        @Override
+        public int compareTo(@NonNull MessageNode messageNode) {
+            Message other = messageNode.mMessage;
+
+            int compared = Long.compare(mMessage.when, other.when);
+            if (compared == 0) {
+                compared = Long.compare(mInsertSeq, messageNode.mInsertSeq);
+            }
+            return compared;
+        }
+    }
+
+    static class StateNode extends StackNode {
+        StateNode(int type) {
+            super(type);
+        }
+    }
+
+    static final class TimedParkStateNode extends StateNode {
+        long mWhenToWake;
+
+        TimedParkStateNode() {
+            super(STACK_NODE_TIMEDPARK);
+        }
+    }
+
+    private static final StateNode sStackStateActive = new StateNode(STACK_NODE_ACTIVE);
+    private static final StateNode sStackStateParked = new StateNode(STACK_NODE_PARKED);
+    private final TimedParkStateNode mStackStateTimedPark = new TimedParkStateNode();
+
+    /* This is the top of our treiber stack. */
+    private static final VarHandle sState;
+    static {
+        try {
+            MethodHandles.Lookup l = MethodHandles.lookup();
+            sState = l.findVarHandle(MessageQueue.class, "mStateValue",
+                    MessageQueue.StackNode.class);
+        } catch (Exception e) {
+            Log.wtf(TAG, "VarHandle lookup failed with exception: " + e);
+            throw new ExceptionInInitializerError(e);
+        }
+    }
+
+    private volatile StackNode mStateValue = sStackStateParked;
+    @GuardedBy("mPriorityQueue")
+    private final PriorityQueue<MessageNode> mPriorityQueue =
+            new PriorityQueue<MessageNode>();
+    @GuardedBy("mPriorityQueue")
+    private final PriorityQueue<MessageNode> mAsyncPriorityQueue =
+            new PriorityQueue<MessageNode>();
+
+    /*
+     * This helps us ensure that messages with the same timestamp are inserted in FIFO order.
+     * Increments on each insert, starting at 0. MessageNode.compareTo() will compare sequences
+     * when delivery timestamps are identical.
+     */
+    private static final VarHandle sNextInsertSeq;
+    private volatile long mNextInsertSeqValue = 0;
+    /*
+     * The exception to the FIFO order rule is sendMessageAtFrontOfQueue().
+     * Those messages must be in LIFO order - SIGH.
+     * Decrements on each front of queue insert.
+     */
+    private static final VarHandle sNextFrontInsertSeq;
+    private volatile long mNextFrontInsertSeqValue = -1;
+    static {
+        try {
+            MethodHandles.Lookup l = MethodHandles.lookup();
+            sNextInsertSeq = l.findVarHandle(MessageQueue.class, "mNextInsertSeqValue",
+                    long.class);
+            sNextFrontInsertSeq = l.findVarHandle(MessageQueue.class, "mNextFrontInsertSeqValue",
+                    long.class);
+        } catch (Exception e) {
+            Log.wtf(TAG, "VarHandle lookup failed with exception: " + e);
+            throw new ExceptionInInitializerError(e);
+        }
+
+    }
+
+    /*
+     * Tracks the number of queued and cancelled messages in our stack.
+     *
+     * On item cancellation, determine whether to wake next() to flush tombstoned messages.
+     * We track queued and cancelled counts as two ints packed into a single long.
+     */
+    private static final class MessageCounts {
+        private static VarHandle sCounts;
+        private volatile long mCountsValue = 0;
+        static {
+            try {
+                MethodHandles.Lookup l = MethodHandles.lookup();
+                sCounts = l.findVarHandle(MessageQueue.MessageCounts.class, "mCountsValue",
+                        long.class);
+            } catch (Exception e) {
+                Log.wtf(TAG, "VarHandle lookup failed with exception: " + e);
+                throw new ExceptionInInitializerError(e);
+            }
+        }
+        /* We use a special value to indicate when next() has been woken for flush. */
+        private static final long AWAKE = Long.MAX_VALUE;
+        /*
+         * Minimum number of messages in the stack which we need before we consider flushing
+         * tombstoned items.
+         */
+        private static final int MESSAGE_FLUSH_THRESHOLD = 10;
+
+        private static int numQueued(long val) {
+            return (int) (val >>> Integer.SIZE);
+        }
+
+        private static int numCancelled(long val) {
+            return (int) val;
+        }
+
+        private static long combineCounts(int queued, int cancelled) {
+            return ((long) queued << Integer.SIZE) | (long) cancelled;
+        }
+
+        public void incrementQueued() {
+            while (true) {
+                long oldVal = mCountsValue;
+                int queued = numQueued(oldVal);
+                int cancelled = numCancelled(oldVal);
+                /* Use Math.max() to avoid overflow of queued count */
+                long newVal = combineCounts(Math.max(queued + 1, queued), cancelled);
+
+                /* Don't overwrite 'AWAKE' state */
+                if (oldVal == AWAKE || sCounts.compareAndSet(this, oldVal, newVal)) {
+                    break;
+                }
+            }
+        }
+
+        public boolean incrementCancelled() {
+            while (true) {
+                long oldVal = mCountsValue;
+                if (oldVal == AWAKE) {
+                    return false;
+                }
+                int queued = numQueued(oldVal);
+                int cancelled = numCancelled(oldVal);
+                boolean needsPurge = queued > MESSAGE_FLUSH_THRESHOLD
+                        && (queued >> 1) < cancelled;
+                long newVal;
+                if (needsPurge) {
+                    newVal = AWAKE;
+                } else {
+                    newVal = combineCounts(queued,
+                            Math.max(cancelled + 1, cancelled));
+                }
+
+                if (sCounts.compareAndSet(this, oldVal, newVal)) {
+                    return needsPurge;
+                }
+            }
+        }
+
+        public void clearCounts() {
+            mCountsValue = 0;
+        }
+    }
+
+    private final MessageCounts mMessageCounts = new MessageCounts();
+
+    private final Object mIdleHandlersLock = new Object();
+    @GuardedBy("mIdleHandlersLock")
+    private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
+    private IdleHandler[] mPendingIdleHandlers;
+
+    private final Object mFileDescriptorRecordsLock = new Object();
+    @GuardedBy("mFileDescriptorRecordsLock")
+    private SparseArray<FileDescriptorRecord> mFileDescriptorRecords;
+
+    private static final VarHandle sQuitting;
+    private boolean mQuittingValue = false;
+    static {
+        try {
+            MethodHandles.Lookup l = MethodHandles.lookup();
+            sQuitting = l.findVarHandle(MessageQueue.class, "mQuittingValue", boolean.class);
+        } catch (Exception e) {
+            Log.wtf(TAG, "VarHandle lookup failed with exception: " + e);
+            throw new ExceptionInInitializerError(e);
+        }
+    }
+
+    // The next barrier token.
+    // Barriers are indicated by messages with a null target whose arg1 field carries the token.
+    private final AtomicInteger mNextBarrierToken = new AtomicInteger(1);
+
+    private static native long nativeInit();
+    private static native void nativeDestroy(long ptr);
+    private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
+    private static native void nativeWake(long ptr);
+    private static native boolean nativeIsPolling(long ptr);
+    private static native void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
+
+    MessageQueue(boolean quitAllowed) {
+        mQuitAllowed = quitAllowed;
+        mPtr = nativeInit();
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            dispose();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    // Disposes of the underlying message queue.
+    // Must only be called on the looper thread or the finalizer.
+    private void dispose() {
+        if (mPtr != 0) {
+            nativeDestroy(mPtr);
+            mPtr = 0;
+        }
+    }
+
+    /**
+     * Returns true if the looper has no pending messages which are due to be processed.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @return True if the looper is idle.
+     */
+    public boolean isIdle() {
+        MessageNode msgNode = null;
+        MessageNode asyncMsgNode = null;
+
+        synchronized (mPriorityQueue) {
+            msgNode = mPriorityQueue.peek();
+            asyncMsgNode = mAsyncPriorityQueue.peek();
+
+            final long now = SystemClock.uptimeMillis();
+            if ((msgNode != null && msgNode.getWhen() <= now)
+                    || (asyncMsgNode != null && asyncMsgNode.getWhen() <= now)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Add a new {@link IdleHandler} to this message queue.  This may be
+     * removed automatically for you by returning false from
+     * {@link IdleHandler#queueIdle IdleHandler.queueIdle()} when it is
+     * invoked, or explicitly removing it with {@link #removeIdleHandler}.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @param handler The IdleHandler to be added.
+     */
+    public void addIdleHandler(@NonNull IdleHandler handler) {
+        if (handler == null) {
+            throw new NullPointerException("Can't add a null IdleHandler");
+        }
+        synchronized (mIdleHandlersLock) {
+            mIdleHandlers.add(handler);
+        }
+    }
+
+    /**
+     * Remove an {@link IdleHandler} from the queue that was previously added
+     * with {@link #addIdleHandler}.  If the given object is not currently
+     * in the idle list, nothing is done.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @param handler The IdleHandler to be removed.
+     */
+    public void removeIdleHandler(@NonNull IdleHandler handler) {
+        synchronized (mIdleHandlersLock) {
+            mIdleHandlers.remove(handler);
+        }
+    }
+
+    /**
+     * Returns whether this looper's thread is currently polling for more work to do.
+     * This is a good signal that the loop is still alive rather than being stuck
+     * handling a callback.  Note that this method is intrinsically racy, since the
+     * state of the loop can change before you get the result back.
+     *
+     * <p>This method is safe to call from any thread.
+     *
+     * @return True if the looper is currently polling for events.
+     * @hide
+     */
+    public boolean isPolling() {
+        // If the loop is quitting then it must not be idling.
+        // We can assume mPtr != 0 when sQuitting is false.
+        return !((boolean) sQuitting.getVolatile(this)) && nativeIsPolling(mPtr);
+    }
+
+    /* Helper to choose the correct queue to insert into. */
+    @GuardedBy("mPriorityQueue")
+    private void insertIntoPriorityQueue(MessageNode msgNode) {
+        if (msgNode.isAsync()) {
+            mAsyncPriorityQueue.offer(msgNode);
+        } else {
+            mPriorityQueue.offer(msgNode);
+        }
+    }
+
+    @GuardedBy("mPriorityQueue")
+    private boolean removeFromPriorityQueue(MessageNode msgNode) {
+        if (msgNode.isAsync()) {
+            return mAsyncPriorityQueue.remove(msgNode);
+        } else {
+            return mPriorityQueue.remove(msgNode);
+        }
+    }
+
+    private MessageNode pickEarliestNode(MessageNode nodeA, MessageNode nodeB) {
+        if (nodeA != null && nodeB != null) {
+            if (nodeA.compareTo(nodeB) < 0) {
+                return nodeA;
+            }
+            return nodeB;
+        }
+
+        return nodeA != null ? nodeA : nodeB;
+    }
+
+    /* Move any non-cancelled messages into the priority queue */
+    private void drainStack(StackNode oldTop) {
+        while (oldTop.isMessageNode()) {
+            MessageNode oldTopMessageNode = (MessageNode) oldTop;
+            if (oldTopMessageNode.removeFromStack()) {
+                insertIntoPriorityQueue(oldTopMessageNode);
+            }
+            MessageNode inserted = oldTopMessageNode;
+            oldTop = oldTopMessageNode.mNext;
+        }
+    }
+
+    /* Set the stack state to Active, return a list of nodes to walk. */
+    private StackNode swapAndSetStackStateActive() {
+        while (true) {
+            /* Set stack state to Active, get node list to walk later */
+            StackNode current = (StackNode) sState.getVolatile(this);
+            if (current == sStackStateActive
+                    || sState.compareAndSet(this, current, sStackStateActive)) {
+                return current;
+            }
+        }
+    }
+
+    /* This is only read/written from the Looper thread */
+    private int mNextPollTimeoutMillis;
+    private static final AtomicLong mMessagesDelivered = new AtomicLong();
+
+    private Message nextMessage() {
+        int i = 0;
+
+        while (true) {
+            if (DEBUG) {
+                Log.d(TAG, "nextMessage loop #" + i);
+                i++;
+            }
+
+            /* This protects us from racing with remove. Enqueue can still add items. */
+            synchronized (mPriorityQueue) {
+
+                /*
+                 * Set our state to active, drain any items from the stack into our priority queues
+                 */
+                StackNode oldTop;
+                oldTop = swapAndSetStackStateActive();
+                drainStack(oldTop);
+
+                /*
+                 * The objective of this next block of code is to:
+                 *  - find a message to return (if any is ready)
+                 *  - find a next message we would like to return, after scheduling.
+                 *     - we make our scheduling decision based on this next message (if it exists).
+                 *
+                 * We have two queues to juggle and the presence of barriers throws an additional
+                 * wrench into our plans.
+                */
+
+                /* Get the first node from each queue */
+                MessageNode msgNode = mPriorityQueue.peek();
+                MessageNode asyncMsgNode = mAsyncPriorityQueue.peek();
+
+                if (DEBUG) {
+                    if (msgNode != null) {
+                        Message msg = msgNode.mMessage;
+                        Log.d(TAG, "Next found node what: " + msg.what + " when: " + msg.when
+                                + " seq: " + msgNode.mInsertSeq + "barrier: "
+                                + msgNode.isBarrier() + " now: "
+                                + SystemClock.uptimeMillis());
+                    }
+                    if (asyncMsgNode != null) {
+                        Message msg = asyncMsgNode.mMessage;
+                        Log.d(TAG, "Next found async node what: " + msg.what + " when: " + msg.when
+                                + " seq: " + asyncMsgNode.mInsertSeq + "barrier: "
+                                + asyncMsgNode.isBarrier() + " now: "
+                                + SystemClock.uptimeMillis());
+                    }
+                }
+
+                /*
+                 * the node which we will return, null if none are ready
+                 */
+                MessageNode found = null;
+                /*
+                 * The node from which we will determine our next wakeup time.
+                 * Null indicates there is no next message ready. If we found a node,
+                 * we can leave this null as Looper will call us again after delivering
+                 * the message.
+                 */
+                MessageNode next = null;
+
+                long now = SystemClock.uptimeMillis();
+                /*
+                 * If we have a barrier we should return the async node if it exists and is
+                 * ready
+                 */
+                if (msgNode != null && msgNode.isBarrier()) {
+                    if (asyncMsgNode != null && now >= asyncMsgNode.getWhen()) {
+                        found = asyncMsgNode;
+                        removeFromPriorityQueue(found);
+                    } else {
+                        next = asyncMsgNode;
+                    }
+                } else { /* No barrier. */
+                    MessageNode earliest;
+                    /*
+                     * If we have two messages, pick the earliest option from either queue.
+                     * Otherwise grab whichever node is non-null. If both are null we'll fall
+                     * through.
+                     */
+                    earliest = pickEarliestNode(msgNode, asyncMsgNode);
+
+                    if (earliest != null) {
+                        if (now >= earliest.getWhen()) {
+                            found = earliest;
+                            removeFromPriorityQueue(found);
+                        } else {
+                            next = earliest;
+                        }
+                    }
+                }
+
+                if (DEBUG) {
+                    if (found != null) {
+                        Message msg = found.mMessage;
+                        Log.d(TAG, "Will deliver node what: " + msg.what + " when: " + msg.when
+                                + " seq: " + found.mInsertSeq + " barrier: "
+                                + found.isBarrier() + " async: " + found.isAsync()
+                                + " now: " + SystemClock.uptimeMillis());
+                    } else {
+                        Log.d(TAG, "No node to deliver");
+                    }
+                    if (next != null) {
+                        Message msg = next.mMessage;
+                        Log.d(TAG, "Next node what: " + msg.what + " when: " + msg.when + " seq: "
+                                + next.mInsertSeq + " barrier: " + next.isBarrier()
+                                + " async: " + next.isAsync()
+                                + " now: " + SystemClock.uptimeMillis());
+                    } else {
+                        Log.d(TAG, "No next node");
+                    }
+                }
+
+                /*
+                 * If we have a found message, we will get called again so there's no need to set
+                 * state.
+                 * In that case we can leave our state as ACTIVE.
+                 *
+                 * Otherwise we should determine how to park the thread.
+                 */
+                StateNode nextOp = sStackStateActive;
+                if (found == null) {
+                    if (next == null) {
+                        /* No message to deliver, sleep indefinitely */
+                        mNextPollTimeoutMillis = -1;
+                        nextOp = sStackStateParked;
+                        if (DEBUG) {
+                            Log.d(TAG, "nextMessage next state is StackStateParked");
+                        }
+                    } else {
+                        /* Message not ready, or we found one to deliver already, set a timeout */
+                        long nextMessageWhen = next.getWhen();
+                        if (nextMessageWhen > now) {
+                            mNextPollTimeoutMillis = (int) Math.min(nextMessageWhen - now,
+                                    Integer.MAX_VALUE);
+                        } else {
+                            mNextPollTimeoutMillis = 0;
+                        }
+
+                        mStackStateTimedPark.mWhenToWake = now + mNextPollTimeoutMillis;
+                        nextOp = mStackStateTimedPark;
+                        if (DEBUG) {
+                            Log.d(TAG, "nextMessage next state is StackStateTimedParked "
+                                    + " next timeout ms " + mNextPollTimeoutMillis
+                                    + " mWhenToWake: " + mStackStateTimedPark.mWhenToWake
+                                    + " now " + now);
+                        }
+                    }
+                }
+
+                /*
+                 * Try to swap our state from Active back to Park or TimedPark. If we raced with
+                 * enqueue, loop back around to pick up any new items.
+                 */
+                if (sState.compareAndSet(this, sStackStateActive, nextOp)) {
+                    mMessageCounts.clearCounts();
+                    if (found != null) {
+                        if (TRACE) {
+                            Trace.setCounter("MQ.Delivered", mMessagesDelivered.incrementAndGet());
+                        }
+                        return found.mMessage;
+                    }
+                    return null;
+                }
+                if (found != null) {
+                    /*
+                     * Add this node back - we will be adding new nodes into our priority queue, and
+                     * recalculating what to return.
+                     */
+                    insertIntoPriorityQueue(found);
+                }
+            }
+        }
+    }
+
+    Message next() {
+        final long ptr = mPtr;
+        if (ptr == 0) {
+            return null;
+        }
+
+        mNextPollTimeoutMillis = 0;
+        int pendingIdleHandlerCount = -1; // -1 only during first iteration
+        while (true) {
+            if (mNextPollTimeoutMillis != 0) {
+                Binder.flushPendingCommands();
+            }
+
+            nativePollOnce(ptr, mNextPollTimeoutMillis);
+
+            Message msg = nextMessage();
+            if (msg != null) {
+                msg.markInUse();
+                return msg;
+            }
+
+            if ((boolean) sQuitting.getVolatile(this)) {
+                return null;
+            }
+
+            synchronized (mIdleHandlersLock) {
+                // If first time idle, then get the number of idlers to run.
+                // Idle handles only run if the queue is empty or if the first message
+                // in the queue (possibly a barrier) is due to be handled in the future.
+                if (pendingIdleHandlerCount < 0
+                        && mNextPollTimeoutMillis != 0) {
+                    pendingIdleHandlerCount = mIdleHandlers.size();
+                }
+                if (pendingIdleHandlerCount <= 0) {
+                    // No idle handlers to run.  Loop and wait some more.
+                    continue;
+                }
+
+                if (mPendingIdleHandlers == null) {
+                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
+                }
+                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
+            }
+
+            // Run the idle handlers.
+            // We only ever reach this code block during the first iteration.
+            for (int i = 0; i < pendingIdleHandlerCount; i++) {
+                final IdleHandler idler = mPendingIdleHandlers[i];
+                mPendingIdleHandlers[i] = null; // release the reference to the handler
+
+                boolean keep = false;
+                try {
+                    keep = idler.queueIdle();
+                } catch (Throwable t) {
+                    Log.wtf(TAG, "IdleHandler threw exception", t);
+                }
+
+                if (!keep) {
+                    synchronized (mIdleHandlersLock) {
+                        mIdleHandlers.remove(idler);
+                    }
+                }
+            }
+
+            // Reset the idle handler count to 0 so we do not run them again.
+            pendingIdleHandlerCount = 0;
+
+            // While calling an idle handler, a new message could have been delivered
+            // so go back and look again for a pending message without waiting.
+            mNextPollTimeoutMillis = 0;
+        }
+    }
+
+    void quit(boolean safe) {
+        if (!mQuitAllowed) {
+            throw new IllegalStateException("Main thread not allowed to quit.");
+        }
+        synchronized (mIdleHandlersLock) {
+            if (sQuitting.compareAndSet(this, false, true)) {
+                if (safe) {
+                    removeAllFutureMessages();
+                } else {
+                    removeAllMessages();
+                }
+
+                // We can assume mPtr != 0 because sQuitting was previously false.
+                nativeWake(mPtr);
+            }
+        }
+    }
+
+    boolean enqueueMessage(@NonNull Message msg, long when) {
+        if (msg.target == null) {
+            throw new IllegalArgumentException("Message must have a target.");
+        }
+
+        if (msg.isInUse()) {
+            throw new IllegalStateException(msg + " This message is already in use.");
+        }
+
+        return enqueueMessageUnchecked(msg, when);
+    }
+
+    private boolean enqueueMessageUnchecked(@NonNull Message msg, long when) {
+        if ((boolean) sQuitting.getVolatile(this)) {
+            IllegalStateException e = new IllegalStateException(
+                    msg.target + " sending message to a Handler on a dead thread");
+            Log.w(TAG, e.getMessage(), e);
+            msg.recycleUnchecked();
+            return false;
+        }
+
+        long seq = when != 0 ? ((long)sNextInsertSeq.getAndAdd(this, 1L) + 1L)
+                : ((long)sNextFrontInsertSeq.getAndAdd(this, -1L) - 1L);
+        /* TODO: Add a MessageNode member to Message so we can avoid this allocation */
+        MessageNode node = new MessageNode(msg, seq);
+        msg.when = when;
+        msg.markInUse();
+
+        if (DEBUG) {
+            Log.d(TAG, "Insert message what: " + msg.what + " when: " + msg.when + " seq: "
+                    + node.mInsertSeq + " barrier: " + node.isBarrier() + " async: "
+                    + node.isAsync() + " now: " + SystemClock.uptimeMillis());
+        }
+
+        while (true) {
+            StackNode old = (StackNode) sState.getVolatile(this);
+            boolean wakeNeeded;
+            boolean inactive;
+
+            node.mNext = old;
+            switch (old.getNodeType()) {
+                case STACK_NODE_ACTIVE:
+                    /*
+                     * The worker thread is currently active and will process any elements added to
+                     * the stack before parking again.
+                     */
+                    node.mBottomOfStack = (StateNode) old;
+                    inactive = false;
+                    node.mWokeUp = true;
+                    wakeNeeded = false;
+                    break;
+
+                case STACK_NODE_PARKED:
+                    node.mBottomOfStack = (StateNode) old;
+                    inactive = true;
+                    node.mWokeUp = true;
+                    wakeNeeded = true;
+                    break;
+
+                case STACK_NODE_TIMEDPARK:
+                    node.mBottomOfStack = (StateNode) old;
+                    inactive = true;
+                    wakeNeeded = mStackStateTimedPark.mWhenToWake >= node.getWhen();
+                    node.mWokeUp = wakeNeeded;
+                    break;
+
+                default:
+                    MessageNode oldMessage = (MessageNode) old;
+
+                    node.mBottomOfStack = oldMessage.mBottomOfStack;
+                    int bottomType = node.mBottomOfStack.getNodeType();
+                    inactive = bottomType >= STACK_NODE_PARKED;
+                    wakeNeeded = (bottomType == STACK_NODE_TIMEDPARK
+                            && mStackStateTimedPark.mWhenToWake >= node.getWhen()
+                            && !oldMessage.mWokeUp);
+                    node.mWokeUp = oldMessage.mWokeUp || wakeNeeded;
+                    break;
+            }
+            if (sState.compareAndSet(this, old, node)) {
+                if (inactive) {
+                    if (wakeNeeded) {
+                        nativeWake(mPtr);
+                    } else {
+                        mMessageCounts.incrementQueued();
+                    }
+                }
+                return true;
+            }
+        }
+    }
+
+    /**
+     * Posts a synchronization barrier to the Looper's message queue.
+     *
+     * Message processing occurs as usual until the message queue encounters the
+     * synchronization barrier that has been posted.  When the barrier is encountered,
+     * later synchronous messages in the queue are stalled (prevented from being executed)
+     * until the barrier is released by calling {@link #removeSyncBarrier} and specifying
+     * the token that identifies the synchronization barrier.
+     *
+     * This method is used to immediately postpone execution of all subsequently posted
+     * synchronous messages until a condition is met that releases the barrier.
+     * Asynchronous messages (see {@link Message#isAsynchronous} are exempt from the barrier
+     * and continue to be processed as usual.
+     *
+     * This call must be always matched by a call to {@link #removeSyncBarrier} with
+     * the same token to ensure that the message queue resumes normal operation.
+     * Otherwise the application will probably hang!
+     *
+     * @return A token that uniquely identifies the barrier.  This token must be
+     * passed to {@link #removeSyncBarrier} to release the barrier.
+     *
+     * @hide
+     */
+    @TestApi
+    public int postSyncBarrier() {
+        return postSyncBarrier(SystemClock.uptimeMillis());
+    }
+
+    private int postSyncBarrier(long when) {
+        final int token = mNextBarrierToken.getAndIncrement();
+        final Message msg = Message.obtain();
+
+        msg.markInUse();
+        msg.arg1 = token;
+
+        if (!enqueueMessageUnchecked(msg, when)) {
+            Log.wtf(TAG, "Unexpected error while adding sync barrier!");
+            return -1;
+        }
+
+        return token;
+    }
+
+    private class MatchBarrierToken extends MessageCompare {
+        int mBarrierToken;
+
+        MatchBarrierToken(int token) {
+            super();
+            mBarrierToken = token;
+        }
+
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == null && m.arg1 == mBarrierToken) {
+                return true;
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Removes a synchronization barrier.
+     *
+     * @param token The synchronization barrier token that was returned by
+     * {@link #postSyncBarrier}.
+     *
+     * @throws IllegalStateException if the barrier was not found.
+     *
+     * @hide
+     */
+    @TestApi
+    public void removeSyncBarrier(int token) {
+        boolean removed;
+        MessageNode first;
+        final MatchBarrierToken matchBarrierToken = new MatchBarrierToken(token);
+
+        synchronized (mPriorityQueue) {
+            try {
+                /* Retain the first element to see if we are currently stuck on a barrier. */
+                first = mPriorityQueue.peek();
+            } catch (NoSuchElementException e) {
+                /* The queue is empty */
+                first = null;
+            }
+
+            removed = findOrRemoveMessages(null, 0, null, null, 0, matchBarrierToken, true);
+            if (removed && first != null) {
+                Message m = first.mMessage;
+                if (m.target == null && m.arg1 == token) {
+                    /* Wake up next() in case it was sleeping on this barrier. */
+                    nativeWake(mPtr);
+                }
+            } else if (!removed) {
+                throw new IllegalStateException("The specified message queue synchronization "
+                        + " barrier token has not been posted or has already been removed.");
+            }
+        }
+    }
+
+    private StateNode getStateNode(StackNode node) {
+        if (node.isMessageNode()) {
+            return ((MessageNode) node).mBottomOfStack;
+        }
+        return (StateNode) node;
+    }
+
+    /*
+     * This class is used to find matches for hasMessages() and removeMessages()
+     */
+    private abstract static class MessageCompare {
+        public abstract boolean compareMessage(Message m, Handler h, int what, Object object,
+                Runnable r, long when);
+    }
+    @GuardedBy("mPriorityQueue")
+    private boolean stackHasMessages(Handler h, int what, Object object, Runnable r, long when,
+            MessageCompare compare, boolean removeMatches) {
+        boolean found = false;
+        StackNode top = (StackNode) sState.getVolatile(this);
+        StateNode bottom = getStateNode(top);
+
+        /* No messages to search. */
+        if (!top.isMessageNode()) {
+            return false;
+        }
+
+        /*
+         * We have messages that we may tombstone. Walk the stack until we hit the bottom.
+         * next() will remove them on it's next pass.
+         */
+        if (!(top instanceof MessageNode)) {
+            Log.wtf(TAG, "Unknown node type found in Trieber stack");
+        }
+        MessageNode p = (MessageNode) top;
+
+        while (true) {
+            if (compare.compareMessage(p.mMessage, h, what, object, r, when)) {
+                found = true;
+                if (DEBUG) {
+                    Log.w(TAG, "stackHasMessages node matches");
+                }
+                if (removeMatches) {
+                    if (p.removeFromStack()) {
+                        p.mMessage.recycleUnchecked();
+                        if (mMessageCounts.incrementCancelled()) {
+                            nativeWake(mPtr);
+                        }
+                    }
+                } else {
+                    return true;
+                }
+            }
+
+            StackNode n = p.mNext;
+            if (!n.isMessageNode()) {
+                /* We reached the end of the stack */
+                return found;
+            }
+            p = (MessageNode) n;
+        }
+    }
+
+    @GuardedBy("mPriorityQueue")
+    private boolean priorityQueueHasMessage(PriorityQueue queue, Handler h,
+            int what, Object object, Runnable r, long when, MessageCompare compare,
+            boolean removeMatches) {
+        Iterator<MessageNode> iterator = queue.iterator();
+        boolean found = false;
+
+        while (iterator.hasNext()) {
+            MessageNode msg = iterator.next();
+
+            if (compare.compareMessage(msg.mMessage, h, what, object, r, when)) {
+                if (removeMatches) {
+                    found = true;
+                    iterator.remove();
+                    msg.mMessage.recycleUnchecked();
+                } else {
+                    return true;
+                }
+            }
+        }
+        return found;
+    }
+
+    private boolean findOrRemoveMessages(Handler h, int what, Object object, Runnable r, long when,
+            MessageCompare compare, boolean removeMatches) {
+        boolean foundInStack, foundInQueue;
+
+        synchronized (mPriorityQueue) {
+            foundInStack = stackHasMessages(h, what, object, r, when, compare, removeMatches);
+            foundInQueue = priorityQueueHasMessage(mPriorityQueue, h, what, object, r, when,
+                    compare, removeMatches);
+            foundInQueue |= priorityQueueHasMessage(mAsyncPriorityQueue, h, what, object, r, when,
+                    compare, removeMatches);
+
+            return foundInStack || foundInQueue;
+        }
+    }
+
+    private static class MatchHandlerWhatAndObject extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.what == what && (object == null || m.obj == object)) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerWhatAndObject mMatchHandlerWhatAndObject =
+            new MatchHandlerWhatAndObject();
+    boolean hasMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return false;
+        }
+
+        return findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObject, false);
+    }
+
+    private static class MatchHandlerWhatAndObjectEquals extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.what == what && (object == null || object.equals(m.obj))) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerWhatAndObjectEquals mMatchHandlerWhatAndObjectEquals =
+            new MatchHandlerWhatAndObjectEquals();
+    boolean hasEqualMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return false;
+        }
+
+        return findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObjectEquals,
+                false);
+    }
+
+    private static class MatchHandlerRunnableAndObject extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.callback == r && (object == null || m.obj == object)) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerRunnableAndObject mMatchHandlerRunnableAndObject =
+            new MatchHandlerRunnableAndObject();
+
+    boolean hasMessages(Handler h, Runnable r, Object object) {
+        if (h == null) {
+            return false;
+        }
+
+        return findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject, false);
+    }
+
+    private static class MatchHandler extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandler mMatchHandler = new MatchHandler();
+    boolean hasMessages(Handler h) {
+        if (h == null) {
+            return false;
+        }
+        return findOrRemoveMessages(h, -1, null, null, 0, mMatchHandler, false);
+    }
+
+    void removeMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return;
+        }
+        findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObject, true);
+    }
+
+    void removeEqualMessages(Handler h, int what, Object object) {
+        if (h == null) {
+            return;
+        }
+        findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObjectEquals, true);
+    }
+
+    void removeMessages(Handler h, Runnable r, Object object) {
+        if (h == null || r == null) {
+            return;
+        }
+        findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject, true);
+    }
+
+    private static class MatchHandlerRunnableAndObjectEquals extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && m.callback == r && (object == null || object.equals(m.obj))) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerRunnableAndObjectEquals mMatchHandlerRunnableAndObjectEquals =
+            new MatchHandlerRunnableAndObjectEquals();
+    void removeEqualMessages(Handler h, Runnable r, Object object) {
+        if (h == null || r == null) {
+            return;
+        }
+        findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObjectEquals, true);
+    }
+
+    private static class MatchHandlerAndObject extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && (object == null || m.obj == object)) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerAndObject mMatchHandlerAndObject = new MatchHandlerAndObject();
+    void removeCallbacksAndMessages(Handler h, Object object) {
+        if (h == null) {
+            return;
+        }
+        findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObject, true);
+    }
+
+    private static class MatchHandlerAndObjectEquals extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.target == h && (object == null || object.equals(m.obj))) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchHandlerAndObjectEquals mMatchHandlerAndObjectEquals =
+            new MatchHandlerAndObjectEquals();
+    void removeCallbacksAndEqualMessages(Handler h, Object object) {
+        if (h == null) {
+            return;
+        }
+        findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObjectEquals, true);
+    }
+
+    private static class MatchAllMessages extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            return true;
+        }
+    }
+    private final MatchAllMessages mMatchAllMessages = new MatchAllMessages();
+    private void removeAllMessages() {
+        findOrRemoveMessages(null, -1, null, null, 0, mMatchAllMessages, true);
+    }
+
+    private static class MatchAllFutureMessages extends MessageCompare {
+        @Override
+        public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r,
+                long when) {
+            if (m.when > when) {
+                return true;
+            }
+            return false;
+        }
+    }
+    private final MatchAllFutureMessages mMatchAllFutureMessages = new MatchAllFutureMessages();
+    private void removeAllFutureMessages() {
+        findOrRemoveMessages(null, -1, null, null, SystemClock.uptimeMillis(),
+                mMatchAllFutureMessages, true);
+    }
+
+    private void printPriorityQueueNodes() {
+        Iterator<MessageNode> iterator = mPriorityQueue.iterator();
+
+        Log.d(TAG, "* Dump priority queue");
+        while (iterator.hasNext()) {
+            MessageNode msgNode = iterator.next();
+            Log.d(TAG, "** MessageNode what: " + msgNode.mMessage.what + " when "
+                    + msgNode.mMessage.when + " seq: " + msgNode.mInsertSeq);
+        }
+    }
+
+    private int dumpPriorityQueue(PriorityQueue<MessageNode> queue, Printer pw, String prefix,
+            Handler h, int n) {
+        int count = 0;
+        long now = SystemClock.uptimeMillis();
+
+        for (MessageNode msgNode : queue) {
+            Message msg = msgNode.mMessage;
+            if (h == null || h == msg.target) {
+                pw.println(prefix + "Message " + (n + count) + ": " + msg.toString(now));
+            }
+            count++;
+        }
+        return count;
+    }
+
+    void dump(Printer pw, String prefix, Handler h) {
+        long now = SystemClock.uptimeMillis();
+        int n = 0;
+
+        pw.println(prefix + "(MessageQueue is using SemiConcurrent implementation)");
+
+        StackNode node = (StackNode) sState.getVolatile(this);
+        while (node != null) {
+            if (node.isMessageNode()) {
+                Message msg = ((MessageNode) node).mMessage;
+                if (h == null || h == msg.target) {
+                    pw.println(prefix + "Message " + n + ": " + msg.toString(now));
+                }
+                node = ((MessageNode) node).mNext;
+            } else {
+                pw.println(prefix + "State: " + node);
+                node = null;
+            }
+            n++;
+        }
+
+        synchronized (mPriorityQueue) {
+            pw.println(prefix + "PriorityQueue Messages: ");
+            n += dumpPriorityQueue(mPriorityQueue, pw, prefix, h, n);
+            pw.println(prefix + "AsyncPriorityQueue Messages: ");
+            n += dumpPriorityQueue(mAsyncPriorityQueue, pw, prefix, h, n);
+        }
+
+        pw.println(prefix + "(Total messages: " + n + ", polling=" + isPolling()
+                + ", quitting=" + (boolean) sQuitting.getVolatile(this) + ")");
+    }
+
+    private int dumpPriorityQueue(PriorityQueue<MessageNode> queue, ProtoOutputStream proto) {
+        int count = 0;
+
+        for (MessageNode msgNode : queue) {
+            Message msg = msgNode.mMessage;
+            msg.dumpDebug(proto, MessageQueueProto.MESSAGES);
+            count++;
+        }
+        return count;
+    }
+
+    void dumpDebug(ProtoOutputStream proto, long fieldId) {
+        final long messageQueueToken = proto.start(fieldId);
+
+        StackNode node = (StackNode) sState.getVolatile(this);
+        while (node.isMessageNode()) {
+            Message msg = ((MessageNode) node).mMessage;
+            msg.dumpDebug(proto, MessageQueueProto.MESSAGES);
+            node = ((MessageNode) node).mNext;
+        }
+
+        synchronized (mPriorityQueue) {
+            dumpPriorityQueue(mPriorityQueue, proto);
+            dumpPriorityQueue(mAsyncPriorityQueue, proto);
+        }
+
+        proto.write(MessageQueueProto.IS_POLLING_LOCKED, isPolling());
+        proto.write(MessageQueueProto.IS_QUITTING, (boolean) sQuitting.getVolatile(this));
+        proto.end(messageQueueToken);
+    }
+
+    /**
+     * Adds a file descriptor listener to receive notification when file descriptor
+     * related events occur.
+     * <p>
+     * If the file descriptor has already been registered, the specified events
+     * and listener will replace any that were previously associated with it.
+     * It is not possible to set more than one listener per file descriptor.
+     * </p><p>
+     * It is important to always unregister the listener when the file descriptor
+     * is no longer of use.
+     * </p>
+     *
+     * @param fd The file descriptor for which a listener will be registered.
+     * @param events The set of events to receive: a combination of the
+     * {@link OnFileDescriptorEventListener#EVENT_INPUT},
+     * {@link OnFileDescriptorEventListener#EVENT_OUTPUT}, and
+     * {@link OnFileDescriptorEventListener#EVENT_ERROR} event masks.  If the requested
+     * set of events is zero, then the listener is unregistered.
+     * @param listener The listener to invoke when file descriptor events occur.
+     *
+     * @see OnFileDescriptorEventListener
+     * @see #removeOnFileDescriptorEventListener
+     */
+    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class)
+    public void addOnFileDescriptorEventListener(@NonNull FileDescriptor fd,
+            @OnFileDescriptorEventListener.Events int events,
+            @NonNull OnFileDescriptorEventListener listener) {
+        if (fd == null) {
+            throw new IllegalArgumentException("fd must not be null");
+        }
+        if (listener == null) {
+            throw new IllegalArgumentException("listener must not be null");
+        }
+
+        synchronized (mFileDescriptorRecordsLock) {
+            updateOnFileDescriptorEventListenerLocked(fd, events, listener);
+        }
+    }
+
+    /**
+     * Removes a file descriptor listener.
+     * <p>
+     * This method does nothing if no listener has been registered for the
+     * specified file descriptor.
+     * </p>
+     *
+     * @param fd The file descriptor whose listener will be unregistered.
+     *
+     * @see OnFileDescriptorEventListener
+     * @see #addOnFileDescriptorEventListener
+     */
+    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class)
+    public void removeOnFileDescriptorEventListener(@NonNull FileDescriptor fd) {
+        if (fd == null) {
+            throw new IllegalArgumentException("fd must not be null");
+        }
+
+        synchronized (mFileDescriptorRecordsLock) {
+            updateOnFileDescriptorEventListenerLocked(fd, 0, null);
+        }
+    }
+
+    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class)
+    private void updateOnFileDescriptorEventListenerLocked(FileDescriptor fd, int events,
+            OnFileDescriptorEventListener listener) {
+        final int fdNum = fd.getInt$();
+
+        int index = -1;
+        FileDescriptorRecord record = null;
+        if (mFileDescriptorRecords != null) {
+            index = mFileDescriptorRecords.indexOfKey(fdNum);
+            if (index >= 0) {
+                record = mFileDescriptorRecords.valueAt(index);
+                if (record != null && record.mEvents == events) {
+                    return;
+                }
+            }
+        }
+
+        if (events != 0) {
+            events |= OnFileDescriptorEventListener.EVENT_ERROR;
+            if (record == null) {
+                if (mFileDescriptorRecords == null) {
+                    mFileDescriptorRecords = new SparseArray<FileDescriptorRecord>();
+                }
+                record = new FileDescriptorRecord(fd, events, listener);
+                mFileDescriptorRecords.put(fdNum, record);
+            } else {
+                record.mListener = listener;
+                record.mEvents = events;
+                record.mSeq += 1;
+            }
+            nativeSetFileDescriptorEvents(mPtr, fdNum, events);
+        } else if (record != null) {
+            record.mEvents = 0;
+            mFileDescriptorRecords.removeAt(index);
+            nativeSetFileDescriptorEvents(mPtr, fdNum, 0);
+        }
+    }
+
+    // Called from native code.
+    private int dispatchEvents(int fd, int events) {
+        // Get the file descriptor record and any state that might change.
+        final FileDescriptorRecord record;
+        final int oldWatchedEvents;
+        final OnFileDescriptorEventListener listener;
+        final int seq;
+        synchronized (mFileDescriptorRecordsLock) {
+            record = mFileDescriptorRecords.get(fd);
+            if (record == null) {
+                return 0; // spurious, no listener registered
+            }
+
+            oldWatchedEvents = record.mEvents;
+            events &= oldWatchedEvents; // filter events based on current watched set
+            if (events == 0) {
+                return oldWatchedEvents; // spurious, watched events changed
+            }
+
+            listener = record.mListener;
+            seq = record.mSeq;
+        }
+
+        // Invoke the listener outside of the lock.
+        int newWatchedEvents = listener.onFileDescriptorEvents(
+                record.mDescriptor, events);
+        if (newWatchedEvents != 0) {
+            newWatchedEvents |= OnFileDescriptorEventListener.EVENT_ERROR;
+        }
+
+        // Update the file descriptor record if the listener changed the set of
+        // events to watch and the listener itself hasn't been updated since.
+        if (newWatchedEvents != oldWatchedEvents) {
+            synchronized (mFileDescriptorRecordsLock) {
+                int index = mFileDescriptorRecords.indexOfKey(fd);
+                if (index >= 0 && mFileDescriptorRecords.valueAt(index) == record
+                        && record.mSeq == seq) {
+                    record.mEvents = newWatchedEvents;
+                    if (newWatchedEvents == 0) {
+                        mFileDescriptorRecords.removeAt(index);
+                    }
+                }
+            }
+        }
+
+        // Return the new set of events to watch for native code to take care of.
+        return newWatchedEvents;
+    }
+
+    /**
+     * Callback interface for discovering when a thread is going to block
+     * waiting for more messages.
+     */
+    public static interface IdleHandler {
+        /**
+         * Called when the message queue has run out of messages and will now
+         * wait for more.  Return true to keep your idle handler active, false
+         * to have it removed.  This may be called if there are still messages
+         * pending in the queue, but they are all scheduled to be dispatched
+         * after the current time.
+         */
+        boolean queueIdle();
+    }
+
+    /**
+     * A listener which is invoked when file descriptor related events occur.
+     */
+    public interface OnFileDescriptorEventListener {
+        /**
+         * File descriptor event: Indicates that the file descriptor is ready for input
+         * operations, such as reading.
+         * <p>
+         * The listener should read all available data from the file descriptor
+         * then return <code>true</code> to keep the listener active or <code>false</code>
+         * to remove the listener.
+         * </p><p>
+         * In the case of a socket, this event may be generated to indicate
+         * that there is at least one incoming connection that the listener
+         * should accept.
+         * </p><p>
+         * This event will only be generated if the {@link #EVENT_INPUT} event mask was
+         * specified when the listener was added.
+         * </p>
+         */
+        public static final int EVENT_INPUT = 1 << 0;
+
+        /**
+         * File descriptor event: Indicates that the file descriptor is ready for output
+         * operations, such as writing.
+         * <p>
+         * The listener should write as much data as it needs.  If it could not
+         * write everything at once, then it should return <code>true</code> to
+         * keep the listener active.  Otherwise, it should return <code>false</code>
+         * to remove the listener then re-register it later when it needs to write
+         * something else.
+         * </p><p>
+         * This event will only be generated if the {@link #EVENT_OUTPUT} event mask was
+         * specified when the listener was added.
+         * </p>
+         */
+        public static final int EVENT_OUTPUT = 1 << 1;
+
+        /**
+         * File descriptor event: Indicates that the file descriptor encountered a
+         * fatal error.
+         * <p>
+         * File descriptor errors can occur for various reasons.  One common error
+         * is when the remote peer of a socket or pipe closes its end of the connection.
+         * </p><p>
+         * This event may be generated at any time regardless of whether the
+         * {@link #EVENT_ERROR} event mask was specified when the listener was added.
+         * </p>
+         */
+        public static final int EVENT_ERROR = 1 << 2;
+
+        /** @hide */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(flag = true, prefix = { "EVENT_" }, value = {
+                EVENT_INPUT,
+                EVENT_OUTPUT,
+                EVENT_ERROR
+        })
+        public @interface Events {}
+
+        /**
+         * Called when a file descriptor receives events.
+         *
+         * @param fd The file descriptor.
+         * @param events The set of events that occurred: a combination of the
+         * {@link #EVENT_INPUT}, {@link #EVENT_OUTPUT}, and {@link #EVENT_ERROR} event masks.
+         * @return The new set of events to watch, or 0 to unregister the listener.
+         *
+         * @see #EVENT_INPUT
+         * @see #EVENT_OUTPUT
+         * @see #EVENT_ERROR
+         */
+        @Events int onFileDescriptorEvents(@NonNull FileDescriptor fd, @Events int events);
+    }
+
+    static final class FileDescriptorRecord {
+        public final FileDescriptor mDescriptor;
+        public int mEvents;
+        public OnFileDescriptorEventListener mListener;
+        public int mSeq;
+
+        public FileDescriptorRecord(FileDescriptor descriptor,
+                int events, OnFileDescriptorEventListener listener) {
+            mDescriptor = descriptor;
+            mEvents = events;
+            mListener = listener;
+        }
+    }
+}
diff --git a/core/java/android/os/connectivity/CellularBatteryStats.java b/core/java/android/os/connectivity/CellularBatteryStats.java
index fc17002..1649ed5 100644
--- a/core/java/android/os/connectivity/CellularBatteryStats.java
+++ b/core/java/android/os/connectivity/CellularBatteryStats.java
@@ -15,11 +15,13 @@
  */
 package android.os.connectivity;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
 import android.os.BatteryStats;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -35,6 +37,7 @@
  *
  * @hide
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 @SystemApi
 public final class CellularBatteryStats implements Parcelable {
 
@@ -83,11 +86,17 @@
                 }
             };
 
-    /** @hide **/
+    /**
+     * This constructor should only be used in tests.
+     * @hide
+     */
+    @FlaggedApi(
+            com.android.server.power.optimization.Flags.FLAG_STREAMLINED_CONNECTIVITY_BATTERY_STATS)
+    @TestApi
     public CellularBatteryStats(long loggingDurationMs, long kernelActiveTimeMs, long numPacketsTx,
             long numBytesTx, long numPacketsRx, long numBytesRx, long sleepTimeMs, long idleTimeMs,
-            long rxTimeMs, Long energyConsumedMaMs, long[] timeInRatMs,
-            long[] timeInRxSignalStrengthLevelMs, long[] txTimeMs,
+            long rxTimeMs, long energyConsumedMaMs, @NonNull long[] timeInRatMs,
+            @NonNull long[] timeInRxSignalStrengthLevelMs, @NonNull long[] txTimeMs,
             long monitoredRailChargeConsumedMaMs) {
 
         mLoggingDurationMs = loggingDurationMs;
@@ -270,7 +279,6 @@
      * @return The amount of time the phone spends in the {@code networkType} network type. The
      * unit is in microseconds.
      */
-    @NonNull
     @SuppressLint("MethodNameUnits")
     public long getTimeInRatMicros(@NetworkType int networkType) {
         if (networkType >= mTimeInRatMs.length) {
@@ -289,7 +297,6 @@
      * @return Amount of time phone spends in specific cellular rx signal strength levels
      * in microseconds. The index is signal strength bin.
      */
-    @NonNull
     @SuppressLint("MethodNameUnits")
     public long getTimeInRxSignalStrengthLevelMicros(
             @IntRange(from = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN,
@@ -315,10 +322,9 @@
      * <li> index 3 = 15dBm < tx_power < 20dBm. </li>
      * <li> index 4 = tx_power > 20dBm. </li>
      * </ul>
-     *
-     * @hide
      */
-    @NonNull
+    @FlaggedApi(
+            com.android.server.power.optimization.Flags.FLAG_STREAMLINED_CONNECTIVITY_BATTERY_STATS)
     public long getTxTimeMillis(
             @IntRange(from = ModemActivityInfo.TX_POWER_LEVEL_0,
                     to = ModemActivityInfo.TX_POWER_LEVEL_4) int level) {
diff --git a/core/java/android/os/connectivity/WifiBatteryStats.java b/core/java/android/os/connectivity/WifiBatteryStats.java
index 7e6ebcf..79e0be8 100644
--- a/core/java/android/os/connectivity/WifiBatteryStats.java
+++ b/core/java/android/os/connectivity/WifiBatteryStats.java
@@ -19,9 +19,11 @@
 import static android.os.BatteryStatsManager.NUM_WIFI_STATES;
 import static android.os.BatteryStatsManager.NUM_WIFI_SUPPL_STATES;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -34,6 +36,7 @@
  * @hide
  */
 @SystemApi
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public final class WifiBatteryStats implements Parcelable {
     private final long mLoggingDurationMillis;
     private final long mKernelActiveTimeMillis;
@@ -150,7 +153,13 @@
                 mMonitoredRailChargeConsumedMaMillis);
     }
 
-    /** @hide **/
+    /**
+     * This constructor should only be used in tests.
+     * @hide
+     */
+    @FlaggedApi(
+            com.android.server.power.optimization.Flags.FLAG_STREAMLINED_CONNECTIVITY_BATTERY_STATS)
+    @TestApi
     public WifiBatteryStats(long loggingDurationMillis, long kernelActiveTimeMillis,
             long numPacketsTx, long numBytesTx, long numPacketsRx, long numBytesRx,
             long sleepTimeMillis, long scanTimeMillis, long idleTimeMillis, long rxTimeMillis,
diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java
index 88bd87e..d019bad 100644
--- a/core/java/android/provider/Telephony.java
+++ b/core/java/android/provider/Telephony.java
@@ -4940,7 +4940,7 @@
          *
          * @hide
          */
-        public static final String COLUMN_IS_NTN = "is_ntn";
+        public static final String COLUMN_IS_ONLY_NTN = "is_only_ntn";
 
         /**
          * TelephonyProvider column name for transferred status
@@ -4976,6 +4976,15 @@
         public static final String COLUMN_SATELLITE_ENTITLEMENT_PLMNS =
                 "satellite_entitlement_plmns";
 
+        /**
+         * TelephonyProvider column name to indicate the satellite ESOS supported. The value of this
+         * column is set based on {@link CarrierConfigManager#KEY_SATELLITE_ESOS_SUPPORTED_BOOL}.
+         * By default, it's disabled.
+         *
+         * @hide
+         */
+        public static final String COLUMN_SATELLITE_ESOS_SUPPORTED = "satellite_esos_supported";
+
         /** All columns in {@link SimInfo} table. */
         private static final List<String> ALL_COLUMNS = List.of(
                 COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID,
@@ -5047,11 +5056,12 @@
                 COLUMN_USER_HANDLE,
                 COLUMN_SATELLITE_ENABLED,
                 COLUMN_SATELLITE_ATTACH_ENABLED_FOR_CARRIER,
-                COLUMN_IS_NTN,
+                COLUMN_IS_ONLY_NTN,
                 COLUMN_SERVICE_CAPABILITIES,
                 COLUMN_TRANSFER_STATUS,
                 COLUMN_SATELLITE_ENTITLEMENT_STATUS,
-                COLUMN_SATELLITE_ENTITLEMENT_PLMNS
+                COLUMN_SATELLITE_ENTITLEMENT_PLMNS,
+                COLUMN_SATELLITE_ESOS_SUPPORTED
         );
 
         /**
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 8ecb1fb..2948129 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -19,6 +19,7 @@
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.service.dreams.Flags.dreamHandlesConfirmKeys;
 import static android.service.dreams.Flags.dreamHandlesBeingObscured;
+import static android.service.dreams.Flags.startAndStopDozingInBackground;
 
 import android.annotation.FlaggedApi;
 import android.annotation.IdRes;
@@ -923,9 +924,16 @@
 
         if (mDozing) {
             try {
-                mDreamManager.startDozing(
+                if (startAndStopDozingInBackground()) {
+                    mDreamManager.startDozingOneway(
                         mDreamToken, mDozeScreenState, mDozeScreenStateReason,
                         mDozeScreenBrightness);
+                } else {
+                    mDreamManager.startDozing(
+                            mDreamToken, mDozeScreenState, mDozeScreenStateReason,
+                            mDozeScreenBrightness);
+                }
+
             } catch (RemoteException ex) {
                 // system server died
             }
@@ -1250,7 +1258,11 @@
         try {
             // finishSelf will unbind the dream controller from the dream service. This will
             // trigger DreamService.this.onDestroy and DreamService.this will die.
-            mDreamManager.finishSelf(mDreamToken, true /*immediate*/);
+            if (startAndStopDozingInBackground()) {
+                mDreamManager.finishSelfOneway(mDreamToken, true /*immediate*/);
+            } else {
+                mDreamManager.finishSelf(mDreamToken, true /*immediate*/);
+            }
         } catch (RemoteException ex) {
             // system server died
         }
diff --git a/core/java/android/service/dreams/IDreamManager.aidl b/core/java/android/service/dreams/IDreamManager.aidl
index cf98bfe0..620eef6 100644
--- a/core/java/android/service/dreams/IDreamManager.aidl
+++ b/core/java/android/service/dreams/IDreamManager.aidl
@@ -50,4 +50,6 @@
     void startDreamActivity(in Intent intent);
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.WRITE_DREAM_STATE)")
     oneway void setDreamIsObscured(in boolean isObscured);
+    oneway void startDozingOneway(in IBinder token, int screenState, int reason, int screenBrightness);
+    oneway void finishSelfOneway(in IBinder token, boolean immediate);
 }
diff --git a/core/java/android/service/dreams/flags.aconfig b/core/java/android/service/dreams/flags.aconfig
index 54d950c..83e0adf 100644
--- a/core/java/android/service/dreams/flags.aconfig
+++ b/core/java/android/service/dreams/flags.aconfig
@@ -47,3 +47,13 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+    name: "start_and_stop_dozing_in_background"
+    namespace: "systemui"
+    description: "Move the start-dozing and stop-dozing operation to the background"
+    bug: "330287187"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/java/android/view/IPinnedTaskListener.aidl b/core/java/android/view/IPinnedTaskListener.aidl
index e4e2d6f..3bd1506 100644
--- a/core/java/android/view/IPinnedTaskListener.aidl
+++ b/core/java/android/view/IPinnedTaskListener.aidl
@@ -42,12 +42,4 @@
      * with fromImeAdjustement set to {@code true}.
      */
     void onImeVisibilityChanged(boolean imeVisible, int imeHeight);
-
-    /**
-     * Called by the window manager to notify the listener that Activity (was or is in pinned mode)
-     * is hidden (either stopped or removed). This is generally used as a signal to reset saved
-     * reentry fraction and size.
-     * {@param componentName} represents the application component of PiP window.
-     */
-    void onActivityHidden(in ComponentName componentName);
 }
diff --git a/core/java/android/view/ImeBackAnimationController.java b/core/java/android/view/ImeBackAnimationController.java
index fc1852d..c7e93c1 100644
--- a/core/java/android/view/ImeBackAnimationController.java
+++ b/core/java/android/view/ImeBackAnimationController.java
@@ -27,6 +27,7 @@
 import android.animation.ValueAnimator;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.WindowConfiguration;
 import android.graphics.Insets;
 import android.util.Log;
 import android.view.animation.BackGestureInterpolator;
@@ -137,9 +138,10 @@
     @Override
     public void onBackInvoked() {
         if (!isBackAnimationAllowed() || !mIsPreCommitAnimationInProgress) {
-            // play regular hide animation if back-animation is not allowed or if insets control has
-            // been cancelled by the system (this can happen in split screen for example)
-            mInsetsController.hide(ime());
+            // play regular hide animation if predictive back-animation is not allowed or if insets
+            // control has been cancelled by the system. This can happen in multi-window mode for
+            // example (i.e. split-screen or activity-embedding)
+            notifyHideIme();
             return;
         }
         startPostCommitAnim(/*hideIme*/ true);
@@ -209,6 +211,11 @@
         if (triggerBack) {
             mInsetsController.setPredictiveBackImeHideAnimInProgress(true);
             notifyHideIme();
+            // requesting IME as invisible during post-commit
+            mInsetsController.setRequestedVisibleTypes(0, ime());
+            // Changes the animation state. This also notifies RootView of changed insets, which
+            // causes it to reset its scrollY to 0f (animated) if it was panned
+            mInsetsController.onAnimationStateChanged(ime(), /*running*/ true);
         }
         if (mStartRootScrollY != 0 && !triggerBack) {
             // This causes RootView to update its scroll back to the panned position
@@ -228,12 +235,6 @@
         // the IME away
         mInsetsController.getHost().getInputMethodManager()
                 .notifyImeHidden(mInsetsController.getHost().getWindowToken(), statsToken);
-
-        // requesting IME as invisible during post-commit
-        mInsetsController.setRequestedVisibleTypes(0, ime());
-        // Changes the animation state. This also notifies RootView of changed insets, which causes
-        // it to reset its scrollY to 0f (animated) if it was panned
-        mInsetsController.onAnimationStateChanged(ime(), /*running*/ true);
     }
 
     private void reset() {
@@ -254,8 +255,18 @@
     }
 
     private boolean isBackAnimationAllowed() {
-        // back animation is allowed in all cases except when softInputMode is adjust_resize AND
-        // there is no app-registered WindowInsetsAnimationCallback AND edge-to-edge is not enabled.
+
+        if (mViewRoot.mContext.getResources().getConfiguration().windowConfiguration
+                .getWindowingMode() == WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW) {
+            // TODO(b/346726115) enable predictive back animation in multi-window mode in
+            //  DisplayImeController
+            return false;
+        }
+
+        // otherwise, the predictive back animation is allowed in all cases except when
+        // 1. softInputMode is adjust_resize AND
+        // 2. there is no app-registered WindowInsetsAnimationCallback AND
+        // 3. edge-to-edge is not enabled.
         return (mViewRoot.mWindowAttributes.softInputMode & SOFT_INPUT_MASK_ADJUST)
                 != SOFT_INPUT_ADJUST_RESIZE
                 || (mViewRoot.mView != null && mViewRoot.mView.hasWindowInsetsAnimationCallback())
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index f653524..634469d 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -72,7 +72,6 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
-import com.android.internal.util.VirtualRefBasePtr;
 import com.android.window.flags.Flags;
 
 import dalvik.system.CloseGuard;
@@ -273,10 +272,12 @@
             String windowName, int displayId);
     private static native void nativeSetFrameTimelineVsync(long transactionObj,
             long frameTimelineVsyncId);
-    private static native void nativeAddJankDataListener(long nativeListener,
-            long nativeSurfaceControl);
-    private static native void nativeRemoveJankDataListener(long nativeListener);
-    private static native long nativeCreateJankDataListenerWrapper(OnJankDataListener listener);
+    private static native long nativeCreateJankDataListenerWrapper(
+            long surfaceControl, OnJankDataListener listener);
+    private static native long nativeGetJankDataListenerWrapperFinalizer();
+    private static native void nativeAddJankDataListener(long nativeListener);
+    private static native void nativeFlushJankData(long nativeListener);
+    private static native void nativeRemoveJankDataListener(long nativeListener, long afterVsync);
     private static native int nativeGetGPUContextPriority();
     private static native void nativeSetTransformHint(long nativeObject,
             @SurfaceControl.BufferTransform int transformHint);
@@ -461,17 +462,63 @@
      * @see #addJankDataListener
      * @hide
      */
-    public static abstract class OnJankDataListener {
-        private final VirtualRefBasePtr mNativePtr;
-
-        public OnJankDataListener() {
-            mNativePtr = new VirtualRefBasePtr(nativeCreateJankDataListenerWrapper(this));
-        }
-
+    public interface OnJankDataListener {
         /**
          * Called when new jank classifications are available.
          */
-        public abstract void onJankDataAvailable(JankData[] jankStats);
+        void onJankDataAvailable(JankData[] jankData);
+
+    }
+
+    /**
+     * Handle to a registered {@link OnJankDatalistener}.
+     * @hide
+     */
+    public static class OnJankDataListenerRegistration {
+        private final long mNativeObject;
+
+        private static final NativeAllocationRegistry sRegistry =
+                NativeAllocationRegistry.createMalloced(
+                       OnJankDataListenerRegistration.class.getClassLoader(),
+                       nativeGetJankDataListenerWrapperFinalizer());
+
+        private final Runnable mFreeNativeResources;
+        private boolean mRemoved = false;
+
+        OnJankDataListenerRegistration(SurfaceControl surface, OnJankDataListener listener) {
+            mNativeObject = nativeCreateJankDataListenerWrapper(surface.mNativeObject, listener);
+            mFreeNativeResources = (mNativeObject == 0) ? () -> {} :
+                    sRegistry.registerNativeAllocation(this, mNativeObject);
+        }
+
+        /**
+         * Request a flush of any pending jank classification data. May cause the registered
+         * listener to be invoked inband.
+         */
+        public void flush() {
+            nativeFlushJankData(mNativeObject);
+        }
+
+        /**
+         * Request the removal of the registered listener after the VSync with the provided ID. Use
+         * a value <= 0 for afterVsync to remove the listener immediately. The given listener will
+         * not be removed before the given VSync, but may still reveive data for frames past the
+         * provided VSync.
+         */
+        public void removeAfter(long afterVsync) {
+            mRemoved = true;
+            nativeRemoveJankDataListener(mNativeObject, afterVsync);
+        }
+
+        /**
+         * Free the native resources associated with the listener registration.
+         */
+        public void release() {
+            if (!mRemoved) {
+                removeAfter(0);
+            }
+            mFreeNativeResources.run();
+        }
     }
 
     private final CloseGuard mCloseGuard = CloseGuard.get();
@@ -2632,19 +2679,11 @@
     }
 
     /**
-     * Adds a callback to be informed about SF's jank classification for a specific surface.
+     * Adds a callback to be informed about SF's jank classification for this surface.
      * @hide
      */
-    public static void addJankDataListener(OnJankDataListener listener, SurfaceControl surface) {
-        nativeAddJankDataListener(listener.mNativePtr.get(), surface.mNativeObject);
-    }
-
-    /**
-     * Removes a jank callback previously added with {@link #addJankDataListener}
-     * @hide
-     */
-    public static void removeJankDataListener(OnJankDataListener listener) {
-        nativeRemoveJankDataListener(listener.mNativePtr.get());
+    public OnJankDataListenerRegistration addJankDataListener(OnJankDataListener listener) {
+        return new OnJankDataListenerRegistration(this, listener);
     }
 
     /**
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 4766942..766e02b 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -13859,6 +13859,11 @@
     })
     @ResolvedLayoutDir
     public int getLayoutDirection() {
+        final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
+        if (targetSdkVersion < Build.VERSION_CODES.JELLY_BEAN_MR1) {
+            mPrivateFlags2 |= PFLAG2_LAYOUT_DIRECTION_RESOLVED;
+            return LAYOUT_DIRECTION_RESOLVED_DEFAULT;
+        }
         return ((mPrivateFlags2 & PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL) ==
                 PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL) ? LAYOUT_DIRECTION_RTL : LAYOUT_DIRECTION_LTR;
     }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index c0bd535..cda58e38 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -7451,6 +7451,10 @@
 
         @Override
         protected int onProcess(QueuedInputEvent q) {
+            if (q.forPreImeOnly()) {
+                // this event is intended for the ViewPreImeInputStage only, let's forward
+                return FORWARD;
+            }
             if (q.mEvent instanceof KeyEvent) {
                 final KeyEvent keyEvent = (KeyEvent) q.mEvent;
 
diff --git a/core/java/android/view/autofill/OWNERS b/core/java/android/view/autofill/OWNERS
index 898947a..7f3b4e5 100644
--- a/core/java/android/view/autofill/OWNERS
+++ b/core/java/android/view/autofill/OWNERS
@@ -1,10 +1,11 @@
 # Bug component: 351486
 
-simranjit@google.com
 haoranzhang@google.com
+jiewenlei@google.com
+simranjit@google.com
 skxu@google.com
+shuc@google.com
 yunicorn@google.com
-reemabajwa@google.com
 
 # Bug component: 543785 = per-file *Augmented*
 per-file *Augmented* = wangqi@google.com
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index 4fb6e69..9b87e23 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -489,7 +489,8 @@
                     return;
                 }
                 OnBackAnimationCallback animationCallback = getBackAnimationCallback();
-                if (animationCallback != null) {
+                if (animationCallback != null
+                        && !(callback instanceof ImeBackAnimationController)) {
                     mProgressAnimator.onBackInvoked(callback::onBackInvoked);
                 } else {
                     mProgressAnimator.reset();
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index 8cd2a3e..68e33c6 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -181,3 +181,14 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    namespace: "windowing_sdk"
+    name: "per_user_display_window_settings"
+    description: "Whether to store display window settings per user to avoid conflicts"
+    bug: "346668297"
+    is_fixed_read_only: true
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java
index 86729f7..bf5df03 100644
--- a/core/java/com/android/internal/jank/FrameTracker.java
+++ b/core/java/com/android/internal/jank/FrameTracker.java
@@ -63,8 +63,8 @@
  * A class that allows the app to get the frame metrics from HardwareRendererObserver.
  * @hide
  */
-public class FrameTracker extends SurfaceControl.OnJankDataListener
-        implements HardwareRendererObserver.OnFrameMetricsAvailableListener {
+public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvailableListener,
+         SurfaceControl.OnJankDataListener {
     private static final String TAG = "FrameTracker";
 
     private static final long INVALID_ID = -1;
@@ -118,6 +118,7 @@
     public final boolean mSurfaceOnly;
 
     private SurfaceControl mSurfaceControl;
+    private SurfaceControl.OnJankDataListenerRegistration mJankDataListenerRegistration;
     private long mBeginVsyncId = INVALID_ID;
     private long mEndVsyncId = INVALID_ID;
     private boolean mMetricsFinalized;
@@ -316,7 +317,8 @@
         Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, name, name, (int) mBeginVsyncId);
         markEvent("FT#beginVsync", mBeginVsyncId);
         markEvent("FT#layerId", mSurfaceControl.getLayerId());
-        mSurfaceControlWrapper.addJankStatsListener(this, mSurfaceControl);
+        mJankDataListenerRegistration =
+                mSurfaceControlWrapper.addJankStatsListener(this, mSurfaceControl);
         if (!mSurfaceOnly) {
             mRendererWrapper.addObserver(mObserver);
         }
@@ -342,6 +344,10 @@
             markEvent("FT#endVsync", mEndVsyncId);
             Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, name, (int) mBeginVsyncId);
 
+            if (mJankDataListenerRegistration != null) {
+                mJankDataListenerRegistration.removeAfter(mEndVsyncId);
+            }
+
             // We don't remove observer here,
             // will remove it when all the frame metrics in this duration are called back.
             // See onFrameMetricsAvailable for the logic of removing the observer.
@@ -358,6 +364,9 @@
                     // Send a flush jank data transaction.
                     if (mSurfaceControl != null && mSurfaceControl.isValid()) {
                         SurfaceControl.Transaction.sendSurfaceFlushJankData(mSurfaceControl);
+                        if (mJankDataListenerRegistration != null) {
+                            mJankDataListenerRegistration.flush();
+                        }
                     }
 
                     long delay;
@@ -680,7 +689,10 @@
     @VisibleForTesting
     @UiThread
     public void removeObservers() {
-        mSurfaceControlWrapper.removeJankStatsListener(this);
+        if (mJankDataListenerRegistration != null) {
+            mJankDataListenerRegistration.release();
+            mJankDataListenerRegistration = null;
+        }
         if (!mSurfaceOnly) {
             // HWUI part.
             mRendererWrapper.removeObserver(mObserver);
@@ -796,14 +808,10 @@
     }
 
     public static class SurfaceControlWrapper {
-
-        public void addJankStatsListener(SurfaceControl.OnJankDataListener listener,
-                SurfaceControl surfaceControl) {
-            SurfaceControl.addJankDataListener(listener, surfaceControl);
-        }
-
-        public void removeJankStatsListener(SurfaceControl.OnJankDataListener listener) {
-            SurfaceControl.removeJankDataListener(listener);
+        /** adds the jank listener to the given surface */
+        public SurfaceControl.OnJankDataListenerRegistration addJankStatsListener(
+                SurfaceControl.OnJankDataListener listener, SurfaceControl surfaceControl) {
+            return surfaceControl.addJankDataListener(listener);
         }
     }
 
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 5365838..9ce7658 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -22,6 +22,7 @@
 #include <android/graphics/properties.h>
 #include <android/graphics/region.h>
 #include <android/gui/BnWindowInfosReportedListener.h>
+#include <android/gui/JankData.h>
 #include <android/hardware/display/IDeviceProductInfoConstants.h>
 #include <android/os/IInputConstants.h>
 #include <android_runtime/AndroidRuntime.h>
@@ -2062,11 +2063,13 @@
         env->DeleteWeakGlobalRef(mOnJankDataListenerWeak);
     }
 
-    void onJankDataAvailable(const std::vector<JankData>& jankData) {
+    bool onJankDataAvailable(const std::vector<gui::JankData>& jankData) override {
         JNIEnv* env = getEnv();
 
         jobject target = env->NewLocalRef(mOnJankDataListenerWeak);
-        if (target == nullptr) return;
+        if (target == nullptr) {
+            return false;
+        }
 
         jobjectArray jJankDataArray = env->NewObjectArray(jankData.size(),
                 gJankDataClassInfo.clazz, nullptr);
@@ -2082,6 +2085,8 @@
                 jJankDataArray);
         env->DeleteLocalRef(jJankDataArray);
         env->DeleteLocalRef(target);
+
+        return true;
     }
 
 private:
@@ -2096,29 +2101,49 @@
     jobject mOnJankDataListenerWeak;
 };
 
-static void nativeAddJankDataListener(JNIEnv* env, jclass clazz,
-                                       jlong jankDataCallbackListenerPtr,
-                                       jlong nativeSurfaceControl) {
+static jlong nativeCreateJankDataListenerWrapper(JNIEnv* env, jclass clazz,
+                                                 jlong nativeSurfaceControl, jobject listener) {
     sp<SurfaceControl> surface(reinterpret_cast<SurfaceControl *>(nativeSurfaceControl));
     if (surface == nullptr) {
+        return 0;
+    }
+
+    sp<JankDataListenerWrapper> wrapper = sp<JankDataListenerWrapper>::make(env, listener);
+    if (wrapper->addListener(std::move(surface)) != OK) {
+        return 0;
+    }
+
+    wrapper->incStrong((void*)nativeCreateJankDataListenerWrapper);
+    return reinterpret_cast<jlong>(wrapper.get());
+}
+
+static void destroyJankDatalistenerWrapper(void* ptr) {
+    JankDataListenerWrapper* wrapper = reinterpret_cast<JankDataListenerWrapper*>(ptr);
+    if (wrapper == nullptr) {
         return;
     }
-    sp<JankDataListenerWrapper> wrapper =
-            reinterpret_cast<JankDataListenerWrapper*>(jankDataCallbackListenerPtr);
-    TransactionCompletedListener::getInstance()->addJankListener(wrapper, surface);
+    wrapper->decStrong((void*)nativeCreateJankDataListenerWrapper);
 }
 
-static void nativeRemoveJankDataListener(JNIEnv* env, jclass clazz,
-                                          jlong jankDataCallbackListenerPtr) {
-    sp<JankDataListenerWrapper> wrapper =
-            reinterpret_cast<JankDataListenerWrapper*>(jankDataCallbackListenerPtr);
-    TransactionCompletedListener::getInstance()->removeJankListener(wrapper);
+static jlong nativeGetJankDataListenerWrapperFinalizer() {
+    return reinterpret_cast<jlong>(&destroyJankDatalistenerWrapper);
 }
 
-static jlong nativeCreateJankDataListenerWrapper(JNIEnv* env, jclass clazz,
-                                                 jobject jankDataListenerObject) {
-    return reinterpret_cast<jlong>(
-            new JankDataListenerWrapper(env, jankDataListenerObject));
+static void nativeFlushJankData(JNIEnv* env, jclass clazz, jlong listener) {
+    sp<JankDataListenerWrapper> wrapper = reinterpret_cast<JankDataListenerWrapper*>(listener);
+    if (wrapper == nullptr) {
+        return;
+    }
+    wrapper->flushJankData();
+}
+
+static void nativeRemoveJankDataListener(JNIEnv* env, jclass clazz, jlong listener,
+                                         jlong afterVsync) {
+    sp<JankDataListenerWrapper> wrapper = reinterpret_cast<JankDataListenerWrapper*>(listener);
+    if (wrapper == nullptr) {
+        return;
+    }
+    wrapper->removeListener(afterVsync);
 }
 
 static jint nativeGetGPUContextPriority(JNIEnv* env, jclass clazz) {
@@ -2436,12 +2461,14 @@
             (void*)nativeRemoveCurrentInputFocus},
     {"nativeSetFrameTimelineVsync", "(JJ)V",
             (void*)nativeSetFrameTimelineVsync },
-    {"nativeAddJankDataListener", "(JJ)V",
-            (void*)nativeAddJankDataListener },
-    {"nativeRemoveJankDataListener", "(J)V",
+    {"nativeFlushJankData", "(J)V",
+            (void*)nativeFlushJankData },
+    {"nativeRemoveJankDataListener", "(JJ)V",
             (void*)nativeRemoveJankDataListener },
-    {"nativeCreateJankDataListenerWrapper", "(Landroid/view/SurfaceControl$OnJankDataListener;)J",
+    {"nativeCreateJankDataListenerWrapper", "(JLandroid/view/SurfaceControl$OnJankDataListener;)J",
             (void*)nativeCreateJankDataListenerWrapper },
+    {"nativeGetJankDataListenerWrapperFinalizer", "()J",
+            (void*)nativeGetJankDataListenerWrapperFinalizer },
     {"nativeGetGPUContextPriority", "()I",
             (void*)nativeGetGPUContextPriority },
     {"nativeSetTransformHint", "(JI)V",
diff --git a/core/res/res/anim/overlay_task_fragment_change.xml b/core/res/res/anim/overlay_task_fragment_change.xml
new file mode 100644
index 0000000..eb02ba8
--- /dev/null
+++ b/core/res/res/anim/overlay_task_fragment_change.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+   -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+    android:showBackdrop="false">
+</set>
\ No newline at end of file
diff --git a/core/res/res/anim/overlay_task_fragment_close_to_bottom.xml b/core/res/res/anim/overlay_task_fragment_close_to_bottom.xml
new file mode 100644
index 0000000..d9487cb
--- /dev/null
+++ b/core/res/res/anim/overlay_task_fragment_close_to_bottom.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+   -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <translate
+        android:fromYDelta="0"
+        android:toYDelta="100%"
+        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:duration="517" />
+</set>
\ No newline at end of file
diff --git a/core/res/res/anim/overlay_task_fragment_close_to_left.xml b/core/res/res/anim/overlay_task_fragment_close_to_left.xml
new file mode 100644
index 0000000..3cdb77a
--- /dev/null
+++ b/core/res/res/anim/overlay_task_fragment_close_to_left.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+   -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <translate
+        android:fromXDelta="0"
+        android:toXDelta="-100%"
+        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:duration="517" />
+</set>
\ No newline at end of file
diff --git a/core/res/res/anim/overlay_task_fragment_close_to_right.xml b/core/res/res/anim/overlay_task_fragment_close_to_right.xml
new file mode 100644
index 0000000..3764561
--- /dev/null
+++ b/core/res/res/anim/overlay_task_fragment_close_to_right.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+   -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <translate
+        android:fromXDelta="0"
+        android:toXDelta="100%"
+        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:duration="517" />
+</set>
\ No newline at end of file
diff --git a/core/res/res/anim/overlay_task_fragment_close_to_top.xml b/core/res/res/anim/overlay_task_fragment_close_to_top.xml
new file mode 100644
index 0000000..a8bfbbd
--- /dev/null
+++ b/core/res/res/anim/overlay_task_fragment_close_to_top.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+   -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <translate
+        android:fromYDelta="0"
+        android:toYDelta="-100%"
+        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:duration="517" />
+</set>
\ No newline at end of file
diff --git a/core/res/res/anim/overlay_task_fragment_open_from_bottom.xml b/core/res/res/anim/overlay_task_fragment_open_from_bottom.xml
new file mode 100644
index 0000000..1d1223f
--- /dev/null
+++ b/core/res/res/anim/overlay_task_fragment_open_from_bottom.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+   -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <translate
+        android:fromYDelta="100%"
+        android:toYDelta="0"
+        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:duration="517" />
+</set>
\ No newline at end of file
diff --git a/core/res/res/anim/overlay_task_fragment_open_from_left.xml b/core/res/res/anim/overlay_task_fragment_open_from_left.xml
new file mode 100644
index 0000000..5e5e080
--- /dev/null
+++ b/core/res/res/anim/overlay_task_fragment_open_from_left.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+   -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <translate
+        android:fromXDelta="-100%"
+        android:toXDelta="0"
+        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:duration="517" />
+</set>
\ No newline at end of file
diff --git a/core/res/res/anim/overlay_task_fragment_open_from_right.xml b/core/res/res/anim/overlay_task_fragment_open_from_right.xml
new file mode 100644
index 0000000..5674ff3
--- /dev/null
+++ b/core/res/res/anim/overlay_task_fragment_open_from_right.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+   -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <translate
+        android:fromXDelta="100%"
+        android:toXDelta="0"
+        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:duration="517" />
+</set>
\ No newline at end of file
diff --git a/core/res/res/anim/overlay_task_fragment_open_from_top.xml b/core/res/res/anim/overlay_task_fragment_open_from_top.xml
new file mode 100644
index 0000000..2e3dd0a
--- /dev/null
+++ b/core/res/res/anim/overlay_task_fragment_open_from_top.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+   -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <translate
+        android:fromYDelta="-100%"
+        android:toYDelta="0"
+        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:duration="517" />
+</set>
\ No newline at end of file
diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml
index 37412a0..f5bb554 100644
--- a/core/res/res/values/colors.xml
+++ b/core/res/res/values/colors.xml
@@ -480,17 +480,17 @@
 
     <!-- Colors used in Android system, from design system.
      These values can be overlaid at runtime by OverlayManager RROs. -->
-    <color name="system_primary_container_light">#5E73A9</color>
-    <color name="system_on_primary_container_light">#FFFFFF</color>
-    <color name="system_primary_light">#2A4174</color>
+    <color name="system_primary_container_light">#D9E2FF</color>
+    <color name="system_on_primary_container_light">#001945</color>
+    <color name="system_primary_light">#475D92</color>
     <color name="system_on_primary_light">#FFFFFF</color>
-    <color name="system_secondary_container_light">#6E7488</color>
-    <color name="system_on_secondary_container_light">#FFFFFF</color>
-    <color name="system_secondary_light">#3C4255</color>
+    <color name="system_secondary_container_light">#DCE2F9</color>
+    <color name="system_on_secondary_container_light">#151B2C</color>
+    <color name="system_secondary_light">#575E71</color>
     <color name="system_on_secondary_light">#FFFFFF</color>
-    <color name="system_tertiary_container_light">#8A6A89</color>
-    <color name="system_on_tertiary_container_light">#FFFFFF</color>
-    <color name="system_tertiary_light">#553A55</color>
+    <color name="system_tertiary_container_light">#FDD7FA</color>
+    <color name="system_on_tertiary_container_light">#2A122C</color>
+    <color name="system_tertiary_light">#725572</color>
     <color name="system_on_tertiary_light">#FFFFFF</color>
     <color name="system_background_light">#FAF8FF</color>
     <color name="system_on_background_light">#1A1B20</color>
@@ -504,17 +504,17 @@
     <color name="system_surface_bright_light">#FAF8FF</color>
     <color name="system_surface_dim_light">#DAD9E0</color>
     <color name="system_surface_variant_light">#E1E2EC</color>
-    <color name="system_on_surface_variant_light">#40434B</color>
-    <color name="system_outline_light">#5D5F67</color>
-    <color name="system_outline_variant_light">#797A83</color>
-    <color name="system_error_light">#8C0009</color>
+    <color name="system_on_surface_variant_light">#44464F</color>
+    <color name="system_outline_light">#757780</color>
+    <color name="system_outline_variant_light">#C5C6D0</color>
+    <color name="system_error_light">#BA1A1A</color>
     <color name="system_on_error_light">#FFFFFF</color>
-    <color name="system_error_container_light">#DA342E</color>
-    <color name="system_on_error_container_light">#FFFFFF</color>
+    <color name="system_error_container_light">#FFDAD6</color>
+    <color name="system_on_error_container_light">#410002</color>
     <color name="system_control_activated_light">#D9E2FF</color>
     <color name="system_control_normal_light">#44464F</color>
     <color name="system_control_highlight_light">#000000</color>
-    <color name="system_text_primary_inverse_light">#E2E2E9</color>
+<color name="system_text_primary_inverse_light">#E2E2E9</color>
     <color name="system_text_secondary_and_tertiary_inverse_light">#C5C6D0</color>
     <color name="system_text_primary_inverse_disable_only_light">#E2E2E9</color>
     <color name="system_text_secondary_and_tertiary_inverse_disabled_light">#E2E2E9</color>
@@ -524,22 +524,22 @@
     <color name="system_palette_key_color_tertiary_light">#8C6D8C</color>
     <color name="system_palette_key_color_neutral_light">#76777D</color>
     <color name="system_palette_key_color_neutral_variant_light">#757780</color>
-    <color name="system_primary_container_dark">#7A90C8</color>
-    <color name="system_on_primary_container_dark">#000000</color>
-    <color name="system_primary_dark">#B7CAFF</color>
-    <color name="system_on_primary_dark">#00143B</color>
-    <color name="system_secondary_container_dark">#8A90A5</color>
-    <color name="system_on_secondary_container_dark">#000000</color>
-    <color name="system_secondary_dark">#C4CAE1</color>
-    <color name="system_on_secondary_dark">#0F1626</color>
-    <color name="system_tertiary_container_dark">#A886A6</color>
-    <color name="system_on_tertiary_container_dark">#000000</color>
-    <color name="system_tertiary_dark">#E4BFE2</color>
-    <color name="system_on_tertiary_dark">#240D26</color>
+    <color name="system_primary_container_dark">#2F4578</color>
+    <color name="system_on_primary_container_dark">#D9E2FF</color>
+    <color name="system_primary_dark">#B0C6FF</color>
+    <color name="system_on_primary_dark">#152E60</color>
+    <color name="system_secondary_container_dark">#404659</color>
+    <color name="system_on_secondary_container_dark">#DCE2F9</color>
+    <color name="system_secondary_dark">#C0C6DC</color>
+    <color name="system_on_secondary_dark">#2A3042</color>
+    <color name="system_tertiary_container_dark">#593D59</color>
+    <color name="system_on_tertiary_container_dark">#FDD7FA</color>
+    <color name="system_tertiary_dark">#E0BBDD</color>
+    <color name="system_on_tertiary_dark">#412742</color>
     <color name="system_background_dark">#121318</color>
     <color name="system_on_background_dark">#E2E2E9</color>
     <color name="system_surface_dark">#121318</color>
-    <color name="system_on_surface_dark">#FCFAFF</color>
+    <color name="system_on_surface_dark">#E2E2E9</color>
     <color name="system_surface_container_low_dark">#1A1B20</color>
     <color name="system_surface_container_lowest_dark">#0C0E13</color>
     <color name="system_surface_container_dark">#1E1F25</color>
@@ -548,13 +548,13 @@
     <color name="system_surface_bright_dark">#38393F</color>
     <color name="system_surface_dim_dark">#121318</color>
     <color name="system_surface_variant_dark">#44464F</color>
-    <color name="system_on_surface_variant_dark">#C9CAD4</color>
-    <color name="system_outline_dark">#A1A2AC</color>
-    <color name="system_outline_variant_dark">#81838C</color>
-    <color name="system_error_dark">#FFBAB1</color>
-    <color name="system_on_error_dark">#370001</color>
-    <color name="system_error_container_dark">#FF5449</color>
-    <color name="system_on_error_container_dark">#000000</color>
+    <color name="system_on_surface_variant_dark">#C5C6D0</color>
+    <color name="system_outline_dark">#8F9099</color>
+    <color name="system_outline_variant_dark">#44464F</color>
+    <color name="system_error_dark">#FFB4AB</color>
+    <color name="system_on_error_dark">#690005</color>
+    <color name="system_error_container_dark">#93000A</color>
+    <color name="system_on_error_container_dark">#FFDAD6</color>
     <color name="system_control_activated_dark">#2F4578</color>
     <color name="system_control_normal_dark">#C5C6D0</color>
     <color name="system_control_highlight_dark">#FFFFFF</color>
@@ -568,63 +568,63 @@
     <color name="system_palette_key_color_tertiary_dark">#8C6D8C</color>
     <color name="system_palette_key_color_neutral_dark">#76777D</color>
     <color name="system_palette_key_color_neutral_variant_dark">#757780</color>
-    <color name="system_primary_fixed">#5E73A9</color>
-    <color name="system_primary_fixed_dim">#455B8F</color>
-    <color name="system_on_primary_fixed">#FFFFFF</color>
-    <color name="system_on_primary_fixed_variant">#FFFFFF</color>
-    <color name="system_secondary_fixed">#6E7488</color>
-    <color name="system_secondary_fixed_dim">#555C6F</color>
-    <color name="system_on_secondary_fixed">#FFFFFF</color>
-    <color name="system_on_secondary_fixed_variant">#FFFFFF</color>
-    <color name="system_tertiary_fixed">#8A6A89</color>
-    <color name="system_tertiary_fixed_dim">#705270</color>
-    <color name="system_on_tertiary_fixed">#FFFFFF</color>
-    <color name="system_on_tertiary_fixed_variant">#FFFFFF</color>
+    <color name="system_primary_fixed">#D9E2FF</color>
+    <color name="system_primary_fixed_dim">#B0C6FF</color>
+    <color name="system_on_primary_fixed">#001945</color>
+    <color name="system_on_primary_fixed_variant">#2F4578</color>
+    <color name="system_secondary_fixed">#DCE2F9</color>
+    <color name="system_secondary_fixed_dim">#C0C6DC</color>
+    <color name="system_on_secondary_fixed">#151B2C</color>
+    <color name="system_on_secondary_fixed_variant">#404659</color>
+    <color name="system_tertiary_fixed">#FDD7FA</color>
+    <color name="system_tertiary_fixed_dim">#E0BBDD</color>
+    <color name="system_on_tertiary_fixed">#2A122C</color>
+    <color name="system_on_tertiary_fixed_variant">#593D59</color>
 
     <!--Colors used in Android system, from design system. These values can be overlaid at runtime
      by OverlayManager RROs.-->
     <color name="system_widget_background_light">#EEF0FF</color>
-    <color name="system_clock_hour_light">#1D2435</color>
-    <color name="system_clock_minute_light">#20386A</color>
-    <color name="system_clock_second_light">#000000</color>
-    <color name="system_theme_app_light">#2F4578</color>
-    <color name="system_on_theme_app_light">#D6DFFF</color>
+    <color name="system_clock_hour_light">#373D50</color>
+    <color name="system_clock_minute_light">#3D5487</color>
+    <color name="system_clock_second_light">#4F659A</color>
+    <color name="system_theme_app_light">#D9E2FF</color>
+    <color name="system_on_theme_app_light">#475D92</color>
     <color name="system_theme_app_ring_light">#94AAE4</color>
-    <color name="system_theme_notif_light">#FDD7FA</color>
-    <color name="system_brand_a_light">#3A5084</color>
+    <color name="system_theme_notif_light">#E0BBDD</color>
+    <color name="system_brand_a_light">#475D92</color>
     <color name="system_brand_b_light">#6E7488</color>
-    <color name="system_brand_c_light">#6076AC</color>
-    <color name="system_brand_d_light">#8C6D8C</color>
+    <color name="system_brand_c_light">#5E73A9</color>
+    <color name="system_brand_d_light">#8A6A89</color>
     <color name="system_under_surface_light">#000000</color>
-    <color name="system_shade_active_light">#D9E2FF</color>
+<color name="system_shade_active_light">#D9E2FF</color>
     <color name="system_on_shade_active_light">#152E60</color>
     <color name="system_on_shade_active_variant_light">#2F4578</color>
     <color name="system_shade_inactive_light">#2F3036</color>
     <color name="system_on_shade_inactive_light">#E1E2EC</color>
     <color name="system_on_shade_inactive_variant_light">#C5C6D0</color>
     <color name="system_shade_disabled_light">#0C0E13</color>
-    <color name="system_overview_background_light">#50525A</color>
+    <color name="system_overview_background_light">#C5C6D0</color>
     <color name="system_widget_background_dark">#152E60</color>
-    <color name="system_clock_hour_dark">#9AA0B6</color>
-    <color name="system_clock_minute_dark">#D8E1FF</color>
-    <color name="system_clock_second_dark">#FFFFFF</color>
-    <color name="system_theme_app_dark">#D9E2FF</color>
-    <color name="system_on_theme_app_dark">#304679</color>
+    <color name="system_clock_hour_dark">#8A90A5</color>
+    <color name="system_clock_minute_dark">#D9E2FF</color>
+    <color name="system_clock_second_dark">#B0C6FF</color>
+    <color name="system_theme_app_dark">#2F4578</color>
+    <color name="system_on_theme_app_dark">#B0C6FF</color>
     <color name="system_theme_app_ring_dark">#94AAE4</color>
-    <color name="system_theme_notif_dark">#E0BBDD</color>
-    <color name="system_brand_a_dark">#90A6DF</color>
-    <color name="system_brand_b_dark">#A4ABC1</color>
+    <color name="system_theme_notif_dark">#FDD7FA</color>
+    <color name="system_brand_a_dark">#B0C6FF</color>
+    <color name="system_brand_b_dark">#DCE2F9</color>
     <color name="system_brand_c_dark">#7A90C8</color>
-    <color name="system_brand_d_dark">#A886A6</color>
+    <color name="system_brand_d_dark">#FDD7FA</color>
     <color name="system_under_surface_dark">#000000</color>
-    <color name="system_shade_active_dark">#D9E2FF</color>
+<color name="system_shade_active_dark">#D9E2FF</color>
     <color name="system_on_shade_active_dark">#001945</color>
     <color name="system_on_shade_active_variant_dark">#2F4578</color>
     <color name="system_shade_inactive_dark">#2F3036</color>
     <color name="system_on_shade_inactive_dark">#E1E2EC</color>
     <color name="system_on_shade_inactive_variant_dark">#C5C6D0</color>
     <color name="system_shade_disabled_dark">#0C0E13</color>
-    <color name="system_overview_background_dark">#C5C6D0</color>
+    <color name="system_overview_background_dark">#50525A</color>
 
     <!-- Accessibility shortcut icon background color -->
     <color name="accessibility_feature_background">#5F6368</color> <!-- Google grey 700 -->
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index fa93e76..f696e872 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -722,6 +722,9 @@
     <!-- label for screenshot item in power menu [CHAR LIMIT=24]-->
     <string name="global_action_screenshot">Screenshot</string>
 
+    <!-- description for mandatory biometrics prompt -->
+    <string name="identity_check_biometric_prompt_description">This is needed since Identity Check is on</string>
+
     <!-- Take bug report menu title [CHAR LIMIT=30] -->
     <string name="bugreport_title">Bug report</string>
     <!-- Message in bugreport dialog describing what it does [CHAR LIMIT=NONE] -->
@@ -6508,17 +6511,17 @@
     <!-- Fingerprint dangling notification title -->
     <string name="fingerprint_dangling_notification_title">Set up Fingerprint Unlock again</string>
     <!-- Fingerprint dangling notification content for only 1 fingerprint deleted -->
-    <string name="fingerprint_dangling_notification_msg_1"><xliff:g id="fingerprint">%s</xliff:g> wasn\'t working well and was deleted</string>
+    <string name="fingerprint_dangling_notification_msg_1"><xliff:g id="fingerprint">%s</xliff:g> can no longer be recognized.</string>
     <!-- Fingerprint dangling notification content for more than 1 fingerprints deleted -->
-    <string name="fingerprint_dangling_notification_msg_2"><xliff:g id="fingerprint">%1$s</xliff:g> and <xliff:g id="fingerprint">%2$s</xliff:g> weren\'t working well and were deleted</string>
+    <string name="fingerprint_dangling_notification_msg_2"><xliff:g id="fingerprint">%1$s</xliff:g> and <xliff:g id="fingerprint">%2$s</xliff:g> can no longer be recognized.</string>
     <!-- Fingerprint dangling notification content for only 1 fingerprint deleted and no fingerprint left-->
-    <string name="fingerprint_dangling_notification_msg_all_deleted_1"><xliff:g id="fingerprint">%s</xliff:g> wasn\'t working well and was deleted. Set it up again to unlock your phone with fingerprint.</string>
+    <string name="fingerprint_dangling_notification_msg_all_deleted_1"><xliff:g id="fingerprint">%s</xliff:g> can no longer be recognized. Set up Fingerprint Unlock again.</string>
     <!-- Fingerprint dangling notification content for more than 1 fingerprints deleted and no fingerprint left  -->
-    <string name="fingerprint_dangling_notification_msg_all_deleted_2"><xliff:g id="fingerprint">%1$s</xliff:g> and <xliff:g id="fingerprint">%2$s</xliff:g> weren\'t working well and were deleted. Set them up again to unlock your phone with your fingerprint.</string>
+    <string name="fingerprint_dangling_notification_msg_all_deleted_2"><xliff:g id="fingerprint">%1$s</xliff:g> and <xliff:g id="fingerprint">%2$s</xliff:g> can no longer be recognized. Set up Fingerprint Unlock again.</string>
     <!-- Face dangling notification title -->
     <string name="face_dangling_notification_title">Set up Face Unlock again</string>
     <!-- Face dangling notification content -->
-    <string name="face_dangling_notification_msg">Your face model wasn\'t working well and was deleted. Set it up again to unlock your phone with face.</string>
+    <string name="face_dangling_notification_msg">Your face model can no longer be recognized. Set up Face Unlock again.</string>
     <!-- Biometric dangling notification "set up" action button -->
     <string name="biometric_dangling_notification_action_set_up">Set up</string>
     <!-- Biometric dangling notification "Not now" action button -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index bdcf13c..7a51abc 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1753,6 +1753,15 @@
   <java-symbol type="anim" name="task_fragment_clear_top_close_exit" />
   <java-symbol type="anim" name="task_fragment_clear_top_open_enter" />
   <java-symbol type="anim" name="task_fragment_clear_top_open_exit" />
+  <java-symbol type="anim" name="overlay_task_fragment_open_from_left" />
+  <java-symbol type="anim" name="overlay_task_fragment_open_from_top" />
+  <java-symbol type="anim" name="overlay_task_fragment_open_from_right" />
+  <java-symbol type="anim" name="overlay_task_fragment_open_from_bottom" />
+  <java-symbol type="anim" name="overlay_task_fragment_change" />
+  <java-symbol type="anim" name="overlay_task_fragment_close_to_left" />
+  <java-symbol type="anim" name="overlay_task_fragment_close_to_top" />
+  <java-symbol type="anim" name="overlay_task_fragment_close_to_right" />
+  <java-symbol type="anim" name="overlay_task_fragment_close_to_bottom" />
 
   <java-symbol type="array" name="config_autoRotationTiltTolerance" />
   <java-symbol type="array" name="config_longPressVibePattern" />
@@ -1918,6 +1927,7 @@
   <java-symbol type="string" name="global_action_voice_assist" />
   <java-symbol type="string" name="global_action_assist" />
   <java-symbol type="string" name="global_action_screenshot" />
+  <java-symbol type="string" name="identity_check_biometric_prompt_description" />
   <java-symbol type="string" name="invalidPuk" />
   <java-symbol type="string" name="lockscreen_carrier_default" />
   <java-symbol type="style" name="Animation.LockScreen" />
diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml
index 67cceb5..581dee5 100644
--- a/core/res/res/xml/sms_short_codes.xml
+++ b/core/res/res/xml/sms_short_codes.xml
@@ -185,6 +185,9 @@
          https://www.itu.int/dms_pub/itu-t/oth/02/02/T020200006B0001PDFE.pdf -->
     <shortcode country="it" pattern="\\d{5}" premium="44[0-4]\\d{2}|47[0-4]\\d{2}|48[0-4]\\d{2}|44[5-9]\\d{4}|47[5-9]\\d{4}|48[5-9]\\d{4}|455\\d{2}|499\\d{2}" free="116\\d{3}|4112503|40\\d{0,12}" standard="430\\d{2}|431\\d{2}|434\\d{4}|435\\d{4}|439\\d{7}" />
 
+    <!-- Jordan: 1-5 digits (standard system default, not country specific) -->
+    <shortcode country="jo" pattern="\\d{1,5}" free="99066" />
+
     <!-- Japan: 8083 used by SOFTBANK_DCB_2 -->
     <shortcode country="jp" pattern="\\d{1,5}" free="8083" />
 
diff --git a/core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java b/core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java
index 58e5be2..4d9b591c 100644
--- a/core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java
+++ b/core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java
@@ -34,6 +34,7 @@
 import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertEquals;
 
+import android.app.WindowConfiguration;
 import android.content.Context;
 import android.graphics.Insets;
 import android.platform.test.annotations.Presubmit;
@@ -102,6 +103,8 @@
             mViewRoot.setOnContentApplyWindowInsetsListener(
                     mock(Window.OnContentApplyWindowInsetsListener.class));
             mBackAnimationController = new ImeBackAnimationController(mViewRoot, mInsetsController);
+            mViewRoot.mContext.getResources().getConfiguration().windowConfiguration
+                    .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN);
 
             when(mWindowInsetsAnimationController.getHiddenStateInsets()).thenReturn(Insets.NONE);
             when(mWindowInsetsAnimationController.getShownStateInsets()).thenReturn(IME_INSETS);
@@ -156,8 +159,28 @@
         mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, 0.5f, EDGE_LEFT));
         // commit back gesture
         mBackAnimationController.onBackInvoked();
-        // verify that InsetsController#hide is called
-        verify(mInsetsController, times(1)).hide(ime());
+        // verify that InputMethodManager#notifyImeHidden is called (which is the case whenever
+        // getInputMethodManager is called from ImeBackAnimationController)
+        verify(mViewRootInsetsControllerHost, times(1)).getInputMethodManager();
+        // verify that ImeBackAnimationController does not take control over IME insets
+        verify(mInsetsController, never()).controlWindowInsetsAnimation(anyInt(), any(), any(),
+                anyBoolean(), anyLong(), any(), anyInt(), anyBoolean());
+    }
+
+    @Test
+    public void testMultiWindowModeNotPlayingAnim() {
+        // setup ViewRoot with WINDOWING_MODE_MULTI_WINDOW
+        mViewRoot.mContext.getResources().getConfiguration().windowConfiguration.setWindowingMode(
+                WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW);
+        // start back gesture
+        mBackAnimationController.onBackStarted(new BackEvent(0f, 0f, 0f, EDGE_LEFT));
+        // progress back gesture
+        mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, 0.5f, EDGE_LEFT));
+        // commit back gesture
+        mBackAnimationController.onBackInvoked();
+        // verify that InputMethodManager#notifyImeHidden is called (which is the case whenever
+        // getInputMethodManager is called from ImeBackAnimationController)
+        verify(mViewRootInsetsControllerHost, times(1)).getInputMethodManager();
         // verify that ImeBackAnimationController does not take control over IME insets
         verify(mInsetsController, never()).controlWindowInsetsAnimation(anyInt(), any(), any(),
                 anyBoolean(), anyLong(), any(), anyInt(), anyBoolean());
@@ -277,9 +300,9 @@
 
             // commit back gesture
             mBackAnimationController.onBackInvoked();
-
-            // verify that InsetsController#hide is called
-            verify(mInsetsController, times(1)).hide(ime());
+            // verify that InputMethodManager#notifyImeHidden is called (which is the case whenever
+            // getInputMethodManager is called from ImeBackAnimationController)
+            verify(mViewRootInsetsControllerHost, times(1)).getInputMethodManager();
         });
     }
 
diff --git a/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java b/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java
index 1a7117e..499caf5 100644
--- a/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java
+++ b/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java
@@ -29,6 +29,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
@@ -83,6 +84,7 @@
     private ChoreographerWrapper mChoreographer;
     private StatsLogWrapper mStatsLog;
     private ArgumentCaptor<OnJankDataListener> mListenerCapture;
+    private SurfaceControl.OnJankDataListenerRegistration mJankStatsRegistration;
     private SurfaceControl mSurfaceControl;
     private FrameTracker.FrameTrackerListener mTrackerListener;
     private ArgumentCaptor<Runnable> mRunnableArgumentCaptor;
@@ -107,10 +109,11 @@
         mSurfaceControlWrapper = mock(SurfaceControlWrapper.class);
 
         mListenerCapture = ArgumentCaptor.forClass(OnJankDataListener.class);
-        doNothing().when(mSurfaceControlWrapper).addJankStatsListener(
+        mJankStatsRegistration = mock(SurfaceControl.OnJankDataListenerRegistration.class);
+        doReturn(mJankStatsRegistration).when(mSurfaceControlWrapper).addJankStatsListener(
                 mListenerCapture.capture(), any());
-        doNothing().when(mSurfaceControlWrapper).removeJankStatsListener(
-                mListenerCapture.capture());
+        doNothing().when(mJankStatsRegistration).flush();
+        doNothing().when(mJankStatsRegistration).removeAfter(anyLong());
 
         mChoreographer = mock(ChoreographerWrapper.class);
         mStatsLog = mock(StatsLogWrapper.class);
@@ -483,7 +486,7 @@
         // an extra frame to trigger finish
         sendFrame(tracker, JANK_NONE, 103L);
 
-        verify(mSurfaceControlWrapper).removeJankStatsListener(any());
+        verify(mJankStatsRegistration).removeAfter(anyLong());
         verify(mTrackerListener).triggerPerfetto(any());
 
         verify(mStatsLog).write(eq(UI_INTERACTION_FRAME_INFO_REPORTED),
@@ -520,7 +523,7 @@
         // an extra frame to trigger finish
         sendFrame(tracker, JANK_NONE, 103L);
 
-        verify(mSurfaceControlWrapper).removeJankStatsListener(any());
+        verify(mJankStatsRegistration).removeAfter(anyLong());
         verify(mTrackerListener, never()).triggerPerfetto(any());
 
         verify(mStatsLog).write(eq(UI_INTERACTION_FRAME_INFO_REPORTED),
@@ -557,7 +560,7 @@
         // janky frame, should be ignored, trigger finish
         sendFrame(tracker, JANK_APP_DEADLINE_MISSED, 103L);
 
-        verify(mSurfaceControlWrapper).removeJankStatsListener(any());
+        verify(mJankStatsRegistration).removeAfter(anyLong());
         verify(mTrackerListener, never()).triggerPerfetto(any());
 
         verify(mStatsLog).write(eq(UI_INTERACTION_FRAME_INFO_REPORTED),
@@ -589,7 +592,7 @@
         tracker.end(FrameTracker.REASON_END_NORMAL);
         sendFrame(tracker, JANK_SURFACEFLINGER_DEADLINE_MISSED, 106L);
         sendFrame(tracker, JANK_SURFACEFLINGER_DEADLINE_MISSED, 107L);
-        verify(mSurfaceControlWrapper).removeJankStatsListener(any());
+        verify(mJankStatsRegistration).removeAfter(anyLong());
         verify(mTrackerListener).triggerPerfetto(any());
         verify(mStatsLog).write(eq(UI_INTERACTION_FRAME_INFO_REPORTED),
                 eq(42), /* displayId */
diff --git a/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java b/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
index 68095e5..f763984 100644
--- a/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
+++ b/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
@@ -183,8 +183,6 @@
         doNothing().when(viewRoot).removeSurfaceChangedCallback(any());
 
         SurfaceControlWrapper surfaceControl = mock(SurfaceControlWrapper.class);
-        doNothing().when(surfaceControl).addJankStatsListener(any(), any());
-        doNothing().when(surfaceControl).removeJankStatsListener(any());
 
         final ChoreographerWrapper choreographer = mock(ChoreographerWrapper.class);
         doReturn(SystemClock.elapsedRealtime()).when(choreographer).getVsyncId();
diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml
index 66b47da..c573cf4a 100644
--- a/data/etc/com.android.systemui.xml
+++ b/data/etc/com.android.systemui.xml
@@ -92,5 +92,6 @@
         <permission name="android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS" />
         <permission name="android.permission.CONTROL_UI_TRACING" />
         <permission name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND" />
+        <permission name="android.permission.SET_BIOMETRIC_DIALOG_ADVANCED" />
     </privapp-permissions>
 </permissions>
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index d0e49d8..eb1fc23 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -20,8 +20,10 @@
 
 import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider;
 import static androidx.window.extensions.embedding.SplitAttributesHelper.isReversedLayout;
+import static androidx.window.extensions.embedding.SplitController.TAG;
 import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
 
+import android.annotation.AnimRes;
 import android.app.Activity;
 import android.app.ActivityThread;
 import android.app.WindowConfiguration;
@@ -31,9 +33,11 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.util.Log;
 import android.util.Pair;
 import android.util.Size;
 import android.view.View;
@@ -56,6 +60,7 @@
 import androidx.window.extensions.layout.WindowLayoutComponentImpl;
 import androidx.window.extensions.layout.WindowLayoutInfo;
 
+import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.window.flags.Flags;
 
@@ -125,6 +130,16 @@
     static final int RESULT_EXPAND_FAILED_NO_TF_INFO = 2;
 
     /**
+     * The key of {@link ActivityStack} alignment relative to its parent container.
+     * <p>
+     * See {@link ContainerPosition} for possible values.
+     * <p>
+     * Note that this constants must align with the definition in WM Jetpack library.
+     */
+    private static final String KEY_ACTIVITY_STACK_ALIGNMENT =
+            "androidx.window.embedding.ActivityStackAlignment";
+
+    /**
      * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
      * Activity, Activity, Intent)}
      */
@@ -649,14 +664,114 @@
         // TODO(b/243518738): Update to resizeTaskFragment after we migrate WCT#setRelativeBounds
         //  and WCT#setWindowingMode to take fragmentToken.
         resizeTaskFragmentIfRegistered(wct, container, relativeBounds);
-        int windowingMode = container.getTaskContainer().getWindowingModeForTaskFragment(
-                relativeBounds);
+        final TaskContainer taskContainer = container.getTaskContainer();
+        final int windowingMode = taskContainer.getWindowingModeForTaskFragment(relativeBounds);
         updateTaskFragmentWindowingModeIfRegistered(wct, container, windowingMode);
-        // Always use default animation for standalone ActivityStack.
-        updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT);
+        if (container.isOverlay() && isOverlayTransitionSupported()) {
+            // Use the overlay transition for the overlay container if it's supported.
+            final TaskFragmentAnimationParams params = createOverlayAnimationParams(relativeBounds,
+                    taskContainer.getBounds(), container);
+            updateAnimationParams(wct, fragmentToken, params);
+        } else {
+            // Otherwise, fallabck to use the default animation params.
+            updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT);
+        }
         setTaskFragmentDimOnTask(wct, fragmentToken, dimOnTask);
     }
 
+    private static boolean isOverlayTransitionSupported() {
+        return Flags.moveAnimationOptionsToChange()
+                && Flags.activityEmbeddingOverlayPresentationFlag();
+    }
+
+    @NonNull
+    private static TaskFragmentAnimationParams createOverlayAnimationParams(
+            @NonNull Rect relativeBounds, @NonNull Rect parentContainerBounds,
+            @NonNull TaskFragmentContainer container) {
+        if (relativeBounds.isEmpty()) {
+            return TaskFragmentAnimationParams.DEFAULT;
+        }
+
+        final int positionFromOptions = container.getLaunchOptions()
+                .getInt(KEY_ACTIVITY_STACK_ALIGNMENT , -1);
+        final int position = positionFromOptions != -1 ? positionFromOptions
+                // Fallback to calculate from bounds if the info can't be retrieved from options.
+                : getOverlayPosition(relativeBounds, parentContainerBounds);
+
+        return new TaskFragmentAnimationParams.Builder()
+                .setOpenAnimationResId(getOpenAnimationResourcesId(position))
+                .setChangeAnimationResId(R.anim.overlay_task_fragment_change)
+                .setCloseAnimationResId(getCloseAnimationResourcesId(position))
+                .build();
+    }
+
+    @VisibleForTesting
+    @ContainerPosition
+    static int getOverlayPosition(
+            @NonNull Rect relativeBounds, @NonNull Rect parentContainerBounds) {
+        final Rect relativeParentBounds = new Rect(parentContainerBounds);
+        relativeParentBounds.offsetTo(0, 0);
+        final int leftMatch = (relativeParentBounds.left == relativeBounds.left) ? 1 : 0;
+        final int topMatch = (relativeParentBounds.top == relativeBounds.top) ? 1 : 0;
+        final int rightMatch = (relativeParentBounds.right == relativeBounds.right) ? 1 : 0;
+        final int bottomMatch = (relativeParentBounds.bottom == relativeBounds.bottom) ? 1 : 0;
+
+        // Flag format: {left|top|right|bottom}. Note that overlay container could be shrunk and
+        // centered, which makes only one of overlay container edge matches the parent container.
+        final int directionFlag = (leftMatch << 3) + (topMatch << 2) + (rightMatch << 1)
+                + bottomMatch;
+
+        final int position = switch (directionFlag) {
+            // Only the left edge match or only the right edge not match: should be on the left of
+            // the parent container.
+            case 0b1000, 0b1101 -> CONTAINER_POSITION_LEFT;
+            // Only the top edge match or only the bottom edge not match: should be on the top of
+            // the parent container.
+            case 0b0100, 0b1110 -> CONTAINER_POSITION_TOP;
+            // Only the right edge match or only the left edge not match: should be on the right of
+            // the parent container.
+            case 0b0010, 0b0111 -> CONTAINER_POSITION_RIGHT;
+            // Only the bottom edge match or only the top edge not match: should be on the bottom of
+            // the parent container.
+            case 0b0001, 0b1011 -> CONTAINER_POSITION_BOTTOM;
+            default -> {
+                Log.w(TAG, "Unsupported position:" + Integer.toBinaryString(directionFlag)
+                        + " fallback to treat it as right. Relative parent bounds: "
+                        + relativeParentBounds + ", relative overlay bounds:" + relativeBounds);
+                yield CONTAINER_POSITION_RIGHT;
+            }
+        };
+        return position;
+    }
+
+    @AnimRes
+    private static int getOpenAnimationResourcesId(@ContainerPosition int position) {
+        return switch (position) {
+            case CONTAINER_POSITION_LEFT -> R.anim.overlay_task_fragment_open_from_left;
+            case CONTAINER_POSITION_TOP -> R.anim.overlay_task_fragment_open_from_top;
+            case CONTAINER_POSITION_RIGHT -> R.anim.overlay_task_fragment_open_from_right;
+            case CONTAINER_POSITION_BOTTOM -> R.anim.overlay_task_fragment_open_from_bottom;
+            default -> {
+                Log.w(TAG, "Unknown position:" + position);
+                yield Resources.ID_NULL;
+            }
+        };
+    }
+
+    @AnimRes
+    private static int getCloseAnimationResourcesId(@ContainerPosition int position) {
+        return switch (position) {
+            case CONTAINER_POSITION_LEFT -> R.anim.overlay_task_fragment_close_to_left;
+            case CONTAINER_POSITION_TOP -> R.anim.overlay_task_fragment_close_to_top;
+            case CONTAINER_POSITION_RIGHT -> R.anim.overlay_task_fragment_close_to_right;
+            case CONTAINER_POSITION_BOTTOM -> R.anim.overlay_task_fragment_close_to_bottom;
+            default -> {
+                Log.w(TAG, "Unknown position:" + position);
+                yield Resources.ID_NULL;
+            }
+        };
+    }
+
     /**
      * Returns the expanded bounds if the {@code relBounds} violate minimum dimension or are not
      * fully covered by the task bounds. Otherwise, returns {@code relBounds}.
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
index 7a0b9a0..3257502 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
@@ -30,6 +30,11 @@
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPlaceholderRuleBuilder;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTfContainer;
+import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM;
+import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT;
+import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT;
+import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP;
+import static androidx.window.extensions.embedding.SplitPresenter.getOverlayPosition;
 import static androidx.window.extensions.embedding.SplitPresenter.sanitizeBounds;
 import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
 
@@ -666,8 +671,8 @@
                 attributes.getRelativeBounds());
         verify(mSplitPresenter).updateTaskFragmentWindowingModeIfRegistered(mTransaction, container,
                 WINDOWING_MODE_MULTI_WINDOW);
-        verify(mSplitPresenter).updateAnimationParams(mTransaction, token,
-                TaskFragmentAnimationParams.DEFAULT);
+        verify(mSplitPresenter).updateAnimationParams(eq(mTransaction), eq(token),
+                any(TaskFragmentAnimationParams.class));
         verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, true);
         verify(mSplitPresenter, never()).setTaskFragmentPinned(any(),
                 any(TaskFragmentContainer.class), anyBoolean());
@@ -691,8 +696,8 @@
                 attributes.getRelativeBounds());
         verify(mSplitPresenter).updateTaskFragmentWindowingModeIfRegistered(mTransaction,
                 container, WINDOWING_MODE_MULTI_WINDOW);
-        verify(mSplitPresenter).updateAnimationParams(mTransaction, token,
-                TaskFragmentAnimationParams.DEFAULT);
+        verify(mSplitPresenter).updateAnimationParams(eq(mTransaction), eq(token),
+                any(TaskFragmentAnimationParams.class));
         verify(mSplitPresenter, never()).setTaskFragmentIsolatedNavigation(any(),
                 any(TaskFragmentContainer.class), anyBoolean());
         verify(mSplitPresenter).setTaskFragmentPinned(mTransaction, container, true);
@@ -870,6 +875,59 @@
                 eq(overlayContainer.getTaskFragmentToken()), eq(activityToken));
     }
 
+    // TODO(b/243518738): Rewrite with TestParameter.
+    @Test
+    public void testGetOverlayPosition() {
+        assertWithMessage("It must be position left for left overlay.")
+                .that(getOverlayPosition(new Rect(
+                        TASK_BOUNDS.left,
+                        TASK_BOUNDS.top,
+                        TASK_BOUNDS.right / 2,
+                        TASK_BOUNDS.bottom), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_LEFT);
+        assertWithMessage("It must be position left for shrunk left overlay.")
+                .that(getOverlayPosition(new Rect(
+                        TASK_BOUNDS.left,
+                        TASK_BOUNDS.top + 20,
+                        TASK_BOUNDS.right / 2,
+                        TASK_BOUNDS.bottom - 20), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_LEFT);
+        assertWithMessage("It must be position left for top overlay.")
+                .that(getOverlayPosition(new Rect(
+                        TASK_BOUNDS.left,
+                        TASK_BOUNDS.top,
+                        TASK_BOUNDS.right,
+                        TASK_BOUNDS.bottom / 2), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_TOP);
+        assertWithMessage("It must be position left for shrunk top overlay.")
+                .that(getOverlayPosition(new Rect(
+                        TASK_BOUNDS.left + 20,
+                        TASK_BOUNDS.top,
+                        TASK_BOUNDS.right - 20,
+                        TASK_BOUNDS.bottom / 2), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_TOP);
+        assertWithMessage("It must be position left for right overlay.")
+                .that(getOverlayPosition(new Rect(
+                        TASK_BOUNDS.right / 2,
+                        TASK_BOUNDS.top,
+                        TASK_BOUNDS.right,
+                        TASK_BOUNDS.bottom), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_RIGHT);
+        assertWithMessage("It must be position left for shrunk right overlay.")
+                .that(getOverlayPosition(new Rect(
+                        TASK_BOUNDS.right / 2,
+                        TASK_BOUNDS.top + 20,
+                        TASK_BOUNDS.right,
+                        TASK_BOUNDS.bottom - 20), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_RIGHT);
+        assertWithMessage("It must be position left for bottom overlay.")
+                .that(getOverlayPosition(new Rect(
+                        TASK_BOUNDS.left,
+                        TASK_BOUNDS.bottom / 2,
+                        TASK_BOUNDS.right,
+                        TASK_BOUNDS.bottom), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_BOTTOM);
+        assertWithMessage("It must be position left for shrunk bottom overlay.")
+                .that(getOverlayPosition(new Rect(
+                        TASK_BOUNDS.left + 20,
+                        TASK_BOUNDS.bottom / 20,
+                        TASK_BOUNDS.right - 20,
+                        TASK_BOUNDS.bottom), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_BOTTOM);
+    }
+
     /**
      * A simplified version of {@link SplitController#createOrUpdateOverlayTaskFragmentIfNeeded}
      */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index 3ded7d2..bebfa90 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -124,6 +124,15 @@
     }
 
     /**
+     * Limited scope callback to notify when a task is removed from the system.  This signal is
+     * not synchronized with anything (or any transition), and should not be used in cases where
+     * that is necessary.
+     */
+    public interface TaskVanishedListener {
+        default void onTaskVanished(RunningTaskInfo taskInfo) {}
+    }
+
+    /**
      * Callbacks for events on a task with a locus id.
      */
     public interface LocusIdListener {
@@ -167,6 +176,9 @@
 
     private final ArraySet<FocusListener> mFocusListeners = new ArraySet<>();
 
+    // Listeners that should be notified when a task is removed
+    private final ArraySet<TaskVanishedListener> mTaskVanishedListeners = new ArraySet<>();
+
     private final Object mLock = new Object();
     private StartingWindowController mStartingWindow;
 
@@ -409,7 +421,7 @@
     }
 
     /**
-     * Removes listener.
+     * Removes a locus id listener.
      */
     public void removeLocusIdListener(LocusIdListener listener) {
         synchronized (mLock) {
@@ -430,7 +442,7 @@
     }
 
     /**
-     * Removes listener.
+     * Removes a focus listener.
      */
     public void removeFocusListener(FocusListener listener) {
         synchronized (mLock) {
@@ -439,6 +451,24 @@
     }
 
     /**
+     * Adds a listener to be notified when a task vanishes.
+     */
+    public void addTaskVanishedListener(TaskVanishedListener listener) {
+        synchronized (mLock) {
+            mTaskVanishedListeners.add(listener);
+        }
+    }
+
+    /**
+     * Removes a task-vanished listener.
+     */
+    public void removeTaskVanishedListener(TaskVanishedListener listener) {
+        synchronized (mLock) {
+            mTaskVanishedListeners.remove(listener);
+        }
+    }
+
+    /**
      * Returns a surface which can be used to attach overlays to the home root task
      */
     @NonNull
@@ -614,6 +644,9 @@
                 t.apply();
                 ProtoLog.v(WM_SHELL_TASK_ORG, "Removing overlay surface");
             }
+            for (TaskVanishedListener l : mTaskVanishedListeners) {
+                l.onTaskVanished(taskInfo);
+            }
 
             if (!ENABLE_SHELL_TRANSITIONS && (appearedInfo.getLeash() != null)) {
                 // Preemptively clean up the leash only if shell transitions are not enabled
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index 5a42817..d270d2b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -265,7 +265,7 @@
         for (TransitionInfo.Change change : openingChanges) {
             final Animation animation =
                     animationProvider.get(info, change, openingWholeScreenBounds);
-            if (animation.getDuration() == 0) {
+            if (shouldUseJumpCutForAnimation(animation)) {
                 continue;
             }
             final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
@@ -290,7 +290,7 @@
             }
             final Animation animation =
                     animationProvider.get(info, change, closingWholeScreenBounds);
-            if (animation.getDuration() == 0) {
+            if (shouldUseJumpCutForAnimation(animation)) {
                 continue;
             }
             final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
@@ -444,8 +444,16 @@
             calculateParentBounds(change, boundsAnimationChange, parentBounds);
             // There are two animations in the array. The first one is for the start leash
             // (snapshot), and the second one is for the end leash (TaskFragment).
-            final Animation[] animations = mAnimationSpec.createChangeBoundsChangeAnimations(change,
-                    parentBounds);
+            final Animation[] animations =
+                    mAnimationSpec.createChangeBoundsChangeAnimations(info, change, parentBounds);
+            // Jump cut if either animation has zero for duration.
+            if (Flags.activityEmbeddingAnimationCustomizationFlag()) {
+                for (Animation animation : animations) {
+                    if (shouldUseJumpCutForAnimation(animation)) {
+                        return new ArrayList<>();
+                    }
+                }
+            }
             // Keep track as we might need to add background color for the animation.
             // Although there may be multiple change animation, record one of them is sufficient
             // because the background color will be added to the root leash for the whole animation.
@@ -492,12 +500,19 @@
                 // window without bounds change.
                 animation = ActivityEmbeddingAnimationSpec.createNoopAnimation(change);
             } else if (TransitionUtil.isClosingType(change.getMode())) {
-                animation = mAnimationSpec.createChangeBoundsCloseAnimation(change, parentBounds);
+                animation =
+                        mAnimationSpec.createChangeBoundsCloseAnimation(info, change, parentBounds);
                 shouldShowBackgroundColor = false;
             } else {
-                animation = mAnimationSpec.createChangeBoundsOpenAnimation(change, parentBounds);
+                animation =
+                        mAnimationSpec.createChangeBoundsOpenAnimation(info, change, parentBounds);
                 shouldShowBackgroundColor = false;
             }
+            if (Flags.activityEmbeddingAnimationCustomizationFlag()) {
+                if (shouldUseJumpCutForAnimation(animation)) {
+                    return new ArrayList<>();
+                }
+            }
             adapters.add(new ActivityEmbeddingAnimationAdapter(animation, change,
                     TransitionUtil.getRootFor(change, info)));
         }
@@ -640,6 +655,12 @@
         return true;
     }
 
+    /** Whether or not to use jump cut based on the animation. */
+    @VisibleForTesting
+    static boolean shouldUseJumpCutForAnimation(@NonNull Animation animation) {
+        return animation.getDuration() == 0;
+    }
+
     /** Updates the changes to end states in {@code startTransaction} for jump cut animation. */
     private void prepareForJumpCut(@NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
index 8d49614..f49b90d0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -46,7 +46,6 @@
 import com.android.wm.shell.shared.TransitionUtil;
 
 /** Animation spec for ActivityEmbedding transition. */
-// TODO(b/206557124): provide an easier way to customize animation
 class ActivityEmbeddingAnimationSpec {
 
     private static final String TAG = "ActivityEmbeddingAnimSpec";
@@ -95,8 +94,14 @@
 
     /** Animation for window that is opening in a change transition. */
     @NonNull
-    Animation createChangeBoundsOpenAnimation(@NonNull TransitionInfo.Change change,
-            @NonNull Rect parentBounds) {
+    Animation createChangeBoundsOpenAnimation(@NonNull TransitionInfo info,
+            @NonNull TransitionInfo.Change change, @NonNull Rect parentBounds) {
+        if (Flags.activityEmbeddingAnimationCustomizationFlag()) {
+            final Animation customAnimation = loadCustomAnimation(info, change);
+            if (customAnimation != null) {
+                return customAnimation;
+            }
+        }
         // Use end bounds for opening.
         final Rect bounds = change.getEndAbsBounds();
         final int startLeft;
@@ -123,8 +128,14 @@
 
     /** Animation for window that is closing in a change transition. */
     @NonNull
-    Animation createChangeBoundsCloseAnimation(@NonNull TransitionInfo.Change change,
-            @NonNull Rect parentBounds) {
+    Animation createChangeBoundsCloseAnimation(@NonNull TransitionInfo info,
+            @NonNull TransitionInfo.Change change, @NonNull Rect parentBounds) {
+        if (Flags.activityEmbeddingAnimationCustomizationFlag()) {
+            final Animation customAnimation = loadCustomAnimation(info, change);
+            if (customAnimation != null) {
+                return customAnimation;
+            }
+        }
         // Use start bounds for closing.
         final Rect bounds = change.getStartAbsBounds();
         final int endTop;
@@ -155,8 +166,17 @@
      *         the second one is for the end leash.
      */
     @NonNull
-    Animation[] createChangeBoundsChangeAnimations(@NonNull TransitionInfo.Change change,
-            @NonNull Rect parentBounds) {
+    Animation[] createChangeBoundsChangeAnimations(@NonNull TransitionInfo info,
+            @NonNull TransitionInfo.Change change, @NonNull Rect parentBounds) {
+        if (Flags.activityEmbeddingAnimationCustomizationFlag()) {
+            // TODO(b/293658614): Support more complicated animations that may need more than a noop
+            // animation as the start leash.
+            final Animation noopAnimation = createNoopAnimation(change);
+            final Animation customAnimation = loadCustomAnimation(info, change);
+            if (customAnimation != null) {
+                return new Animation[]{noopAnimation, customAnimation};
+            }
+        }
         // Both start bounds and end bounds are in screen coordinates. We will post translate
         // to the local coordinates in ActivityEmbeddingAnimationAdapter#onAnimationUpdate
         final Rect startBounds = change.getStartAbsBounds();
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 43cdcca..4ea41d5 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
@@ -646,6 +646,7 @@
             ShellInit shellInit,
             ShellController shellController,
             ShellCommandHandler shellCommandHandler,
+            ShellTaskOrganizer shellTaskOrganizer,
             DisplayController displayController,
             UiEventLogger uiEventLogger,
             IconProvider iconProvider,
@@ -653,8 +654,8 @@
             Transitions transitions,
             @ShellMainThread ShellExecutor mainExecutor) {
         return new DragAndDropController(context, shellInit, shellController, shellCommandHandler,
-                displayController, uiEventLogger, iconProvider, globalDragListener, transitions,
-                mainExecutor);
+                shellTaskOrganizer, displayController, uiEventLogger, iconProvider,
+                globalDragListener, transitions, mainExecutor);
     }
 
     //
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
index c374eb8..a4813a3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
@@ -62,6 +62,7 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.launcher3.icons.IconProvider;
 import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.RemoteCallable;
@@ -85,6 +86,7 @@
 public class DragAndDropController implements RemoteCallable<DragAndDropController>,
         GlobalDragListener.GlobalDragListenerCallback,
         DisplayController.OnDisplaysChangedListener,
+        ShellTaskOrganizer.TaskVanishedListener,
         View.OnDragListener, ComponentCallbacks2 {
 
     private static final String TAG = DragAndDropController.class.getSimpleName();
@@ -92,6 +94,7 @@
     private final Context mContext;
     private final ShellController mShellController;
     private final ShellCommandHandler mShellCommandHandler;
+    private final ShellTaskOrganizer mShellTaskOrganizer;
     private final DisplayController mDisplayController;
     private final DragAndDropEventLogger mLogger;
     private final IconProvider mIconProvider;
@@ -133,6 +136,7 @@
             ShellInit shellInit,
             ShellController shellController,
             ShellCommandHandler shellCommandHandler,
+            ShellTaskOrganizer shellTaskOrganizer,
             DisplayController displayController,
             UiEventLogger uiEventLogger,
             IconProvider iconProvider,
@@ -142,6 +146,7 @@
         mContext = context;
         mShellController = shellController;
         mShellCommandHandler = shellCommandHandler;
+        mShellTaskOrganizer = shellTaskOrganizer;
         mDisplayController = displayController;
         mLogger = new DragAndDropEventLogger(uiEventLogger);
         mIconProvider = iconProvider;
@@ -163,6 +168,7 @@
         }, 0);
         mShellController.addExternalInterface(KEY_EXTRA_SHELL_DRAG_AND_DROP,
                 this::createExternalInterface, this);
+        mShellTaskOrganizer.addTaskVanishedListener(this);
         mShellCommandHandler.addDumpCallback(this::dump, this);
         mGlobalDragListener.setListener(this);
     }
@@ -281,6 +287,34 @@
     }
 
     @Override
+    public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+        if (taskInfo.baseIntent == null) {
+            // Invalid info
+            return;
+        }
+        // Find the active drag
+        PerDisplay pd = null;
+        for (int i = 0; i < mDisplayDropTargets.size(); i++) {
+            final PerDisplay iPd = mDisplayDropTargets.valueAt(i);
+            if (iPd.isHandlingDrag) {
+                pd = iPd;
+                break;
+            }
+        }
+        if (pd == null || !pd.isHandlingDrag) {
+            // Not currently dragging
+            return;
+        }
+
+        // Update the drag session
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+                "Handling vanished task: id=%d component=%s", taskInfo.taskId,
+                taskInfo.baseIntent.getComponent());
+        pd.dragSession.updateRunningTask();
+        pd.dragLayout.updateSession(pd.dragSession);
+    }
+
+    @Override
     public boolean onDrag(View target, DragEvent event) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
                 "Drag event: action=%s x=%f y=%f xOffset=%f yOffset=%f",
@@ -313,11 +347,10 @@
                     Slog.w(TAG, "Unexpected drag start during an active drag");
                     return false;
                 }
-                // TODO(b/290391688): Also update the session data with task stack changes
                 pd.dragSession = new DragSession(ActivityTaskManager.getInstance(),
                         mDisplayController.getDisplayLayout(displayId), event.getClipData(),
                         event.getDragFlags());
-                pd.dragSession.update();
+                pd.dragSession.initialize();
                 pd.activeDragCount++;
                 pd.dragLayout.prepare(pd.dragSession, mLogger.logStart(pd.dragSession));
                 setDropTargetWindowVisibility(pd, View.VISIBLE);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
index a42ca19..b1882fc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
@@ -84,7 +84,10 @@
     private static final String TAG = DragAndDropPolicy.class.getSimpleName();
 
     private final Context mContext;
-    private final Starter mStarter;
+    // Used only for launching a fullscreen task (or as a fallback if there is no split starter)
+    private final Starter mFullscreenStarter;
+    // Used for launching tasks into splitscreen
+    private final Starter mSplitscreenStarter;
     private final SplitScreenController mSplitScreen;
     private final ArrayList<DragAndDropPolicy.Target> mTargets = new ArrayList<>();
     private final RectF mDisallowHitRegion = new RectF();
@@ -97,10 +100,12 @@
     }
 
     @VisibleForTesting
-    DragAndDropPolicy(Context context, SplitScreenController splitScreen, Starter starter) {
+    DragAndDropPolicy(Context context, SplitScreenController splitScreen,
+            Starter fullscreenStarter) {
         mContext = context;
         mSplitScreen = splitScreen;
-        mStarter = mSplitScreen != null ? mSplitScreen : starter;
+        mFullscreenStarter = fullscreenStarter;
+        mSplitscreenStarter = splitScreen;
     }
 
     /**
@@ -245,17 +250,20 @@
             mSplitScreen.onDroppedToSplit(position, mLoggerSessionId);
         }
 
+        final Starter starter = target.type == TYPE_FULLSCREEN
+                ? mFullscreenStarter
+                : mSplitscreenStarter;
         if (mSession.appData != null) {
-            launchApp(mSession, position);
+            launchApp(mSession, starter, position);
         } else {
-            launchIntent(mSession, position);
+            launchIntent(mSession, starter, position);
         }
     }
 
     /**
      * Launches an app provided by SysUI.
      */
-    private void launchApp(DragSession session, @SplitPosition int position) {
+    private void launchApp(DragSession session, Starter starter, @SplitPosition int position) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Launching app data at position=%d",
                 position);
         final ClipDescription description = session.getClipDescription();
@@ -275,11 +283,11 @@
 
         if (isTask) {
             final int taskId = session.appData.getIntExtra(EXTRA_TASK_ID, INVALID_TASK_ID);
-            mStarter.startTask(taskId, position, opts);
+            starter.startTask(taskId, position, opts);
         } else if (isShortcut) {
             final String packageName = session.appData.getStringExtra(EXTRA_PACKAGE_NAME);
             final String id = session.appData.getStringExtra(EXTRA_SHORTCUT_ID);
-            mStarter.startShortcut(packageName, id, position, opts, user);
+            starter.startShortcut(packageName, id, position, opts, user);
         } else {
             final PendingIntent launchIntent =
                     session.appData.getParcelableExtra(EXTRA_PENDING_INTENT);
@@ -288,7 +296,7 @@
                     Log.e(TAG, "Expected app intent's EXTRA_USER to match pending intent user");
                 }
             }
-            mStarter.startIntent(launchIntent, user.getIdentifier(), null /* fillIntent */,
+            starter.startIntent(launchIntent, user.getIdentifier(), null /* fillIntent */,
                     position, opts);
         }
     }
@@ -296,7 +304,7 @@
     /**
      * Launches an intent sender provided by an application.
      */
-    private void launchIntent(DragSession session, @SplitPosition int position) {
+    private void launchIntent(DragSession session, Starter starter, @SplitPosition int position) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Launching intent at position=%d",
                 position);
         final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic();
@@ -309,7 +317,7 @@
                 | FLAG_ACTIVITY_MULTIPLE_TASK);
 
         final Bundle opts = baseActivityOpts.toBundle();
-        mStarter.startIntent(session.launchableIntent,
+        starter.startIntent(session.launchableIntent,
                 session.launchableIntent.getCreatorUserHandle().getIdentifier(),
                 null /* fillIntent */, position, opts);
     }
@@ -420,7 +428,7 @@
 
         @Override
         public String toString() {
-            return "Target {hit=" + hitRegion + " draw=" + drawRegion + "}";
+            return "Target {type=" + type + " hit=" + hitRegion + " draw=" + drawRegion + "}";
         }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
index 4bb10df..5df83be 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
@@ -42,6 +42,7 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Insets;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.graphics.drawable.Drawable;
@@ -102,6 +103,8 @@
     private boolean mIsShowing;
     private boolean mHasDropped;
     private DragSession mSession;
+    // The last position that was handled by the drag layout
+    private final Point mLastPosition = new Point();
 
     @SuppressLint("WrongConstant")
     public DragLayout(Context context, SplitScreenController splitScreenController,
@@ -265,6 +268,15 @@
      */
     public void prepare(DragSession session, InstanceId loggerSessionId) {
         mPolicy.start(session, loggerSessionId);
+        updateSession(session);
+    }
+
+    /**
+     * Updates the drag layout based on the diven drag session.
+     */
+    public void updateSession(DragSession session) {
+        // Note: The policy currently just keeps a reference to the session
+        boolean updatingExistingSession = mSession != null;
         mSession = session;
         mHasDropped = false;
         mCurrentTarget = null;
@@ -312,6 +324,11 @@
             updateDropZoneSizes(topOrLeftBounds, bottomOrRightBounds);
         }
         requestLayout();
+        if (updatingExistingSession) {
+            // Update targets if we are already currently dragging
+            recomputeDropTargets();
+            update(mLastPosition.x, mLastPosition.y);
+        }
     }
 
     private void updateDropZoneSizesForSingleTask() {
@@ -359,6 +376,9 @@
         mDropZoneView2.setLayoutParams(dropZoneView2);
     }
 
+    /**
+     * Shows the drag layout.
+     */
     public void show() {
         mIsShowing = true;
         recomputeDropTargets();
@@ -384,13 +404,19 @@
      * Updates the visible drop target as the user drags.
      */
     public void update(DragEvent event) {
+        update((int) event.getX(), (int) event.getY());
+    }
+
+    /**
+     * Updates the visible drop target as the user drags to the given coordinates.
+     */
+    private void update(int x, int y) {
         if (mHasDropped) {
             return;
         }
         // Find containing region, if the same as mCurrentRegion, then skip, otherwise, animate the
         // visibility of the current region
-        DragAndDropPolicy.Target target = mPolicy.getTargetAtLocation(
-                (int) event.getX(), (int) event.getY());
+        DragAndDropPolicy.Target target = mPolicy.getTargetAtLocation(x, y);
         if (mCurrentTarget != target) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Current target: %s", target);
             if (target == null) {
@@ -429,6 +455,7 @@
             }
             mCurrentTarget = target;
         }
+        mLastPosition.set(x, y);
     }
 
     /**
@@ -436,6 +463,7 @@
      */
     public void hide(DragEvent event, Runnable hideCompleteCallback) {
         mIsShowing = false;
+        mLastPosition.set(-1, -1);
         animateSplitContainers(false, () -> {
             if (hideCompleteCallback != null) {
                 hideCompleteCallback.run();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java
index 0addd43..41a50b1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java
@@ -30,7 +30,9 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
 import java.util.List;
 
@@ -79,17 +81,27 @@
     }
 
     /**
-     * Updates the session data based on the current state of the system.
+     * Updates the running task for this drag session.
      */
-    void update() {
-        List<ActivityManager.RunningTaskInfo> tasks =
+    void updateRunningTask() {
+        final List<ActivityManager.RunningTaskInfo> tasks =
                 mActivityTaskManager.getTasks(1, false /* filterOnlyVisibleRecents */);
         if (!tasks.isEmpty()) {
             final ActivityManager.RunningTaskInfo task = tasks.get(0);
             runningTaskInfo = task;
             runningTaskWinMode = task.getWindowingMode();
             runningTaskActType = task.getActivityType();
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+                    "Running task: id=%d component=%s", task.taskId,
+                    task.baseIntent != null ? task.baseIntent.getComponent() : "null");
         }
+    }
+
+    /**
+     * Updates the session data based on the current state of the system at the start of the drag.
+     */
+    void initialize() {
+        updateRunningTask();
 
         activityInfo = mInitialDragData.getItemAt(0).getActivityInfo();
         // TODO: This should technically check & respect config_supportsNonResizableMultiWindow
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java
index 724a130..2ccadb8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java
@@ -19,6 +19,7 @@
 import static com.android.wm.shell.animation.Interpolators.FAST_OUT_SLOW_IN;
 
 import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.graphics.Canvas;
@@ -37,13 +38,16 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.R;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
 /**
  * Renders a drop zone area for items being dragged.
  */
 public class DropZoneView extends FrameLayout {
 
+    private static final boolean DEBUG_LAYOUT = false;
     private static final float SPLASHSCREEN_ALPHA = 0.90f;
     private static final float HIGHLIGHT_ALPHA = 1f;
     private static final int MARGIN_ANIMATION_ENTER_DURATION = 400;
@@ -77,6 +81,7 @@
     private int mHighlightColor;
 
     private ObjectAnimator mBackgroundAnimator;
+    private int mTargetBackgroundColor;
     private ObjectAnimator mMarginAnimator;
     private float mMarginPercent;
 
@@ -181,6 +186,9 @@
 
     /** Animates between highlight and splashscreen depending on current state. */
     public void animateSwitch() {
+        if (DEBUG_LAYOUT) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "animateSwitch");
+        }
         mShowingHighlight = !mShowingHighlight;
         mShowingSplash = !mShowingHighlight;
         final int newColor = mShowingHighlight ? mHighlightColor : mSplashScreenColor;
@@ -190,6 +198,10 @@
 
     /** Animates the highlight indicating the zone is hovered on or not. */
     public void setShowingHighlight(boolean showingHighlight) {
+        if (DEBUG_LAYOUT) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "setShowingHighlight: showing=%b",
+                    showingHighlight);
+        }
         mShowingHighlight = showingHighlight;
         mShowingSplash = !mShowingHighlight;
         final int newColor = mShowingHighlight ? mHighlightColor : mSplashScreenColor;
@@ -199,6 +211,10 @@
 
     /** Animates the margins around the drop zone to show or hide. */
     public void setShowingMargin(boolean visible) {
+        if (DEBUG_LAYOUT) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "setShowingMargin: visible=%b",
+                    visible);
+        }
         if (mShowingMargin != visible) {
             mShowingMargin = visible;
             animateMarginToState();
@@ -212,6 +228,15 @@
     }
 
     private void animateBackground(int startColor, int endColor) {
+        if (DEBUG_LAYOUT) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+                    "animateBackground: start=%s end=%s",
+                    Integer.toHexString(startColor), Integer.toHexString(endColor));
+        }
+        if (endColor == mTargetBackgroundColor) {
+            // Already at, or animating to, that background color
+            return;
+        }
         if (mBackgroundAnimator != null) {
             mBackgroundAnimator.cancel();
         }
@@ -223,6 +248,7 @@
             mBackgroundAnimator.setInterpolator(FAST_OUT_SLOW_IN);
         }
         mBackgroundAnimator.start();
+        mTargetBackgroundColor = endColor;
     }
 
     private void animateSplashScreenIcon() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java
index ce98458..93ede7a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PinnedStackListenerForwarder.java
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.pip;
 
-import android.content.ComponentName;
 import android.os.RemoteException;
 import android.view.IPinnedTaskListener;
 import android.view.WindowManagerGlobal;
@@ -70,12 +69,6 @@
         }
     }
 
-    private void onActivityHidden(ComponentName componentName) {
-        for (PinnedTaskListener listener : mListeners) {
-            listener.onActivityHidden(componentName);
-        }
-    }
-
     @BinderThread
     private class PinnedTaskListenerImpl extends IPinnedTaskListener.Stub {
         @Override
@@ -91,13 +84,6 @@
                 PinnedStackListenerForwarder.this.onImeVisibilityChanged(imeVisible, imeHeight);
             });
         }
-
-        @Override
-        public void onActivityHidden(ComponentName componentName) {
-            mMainExecutor.execute(() -> {
-                PinnedStackListenerForwarder.this.onActivityHidden(componentName);
-            });
-        }
     }
 
     /**
@@ -108,7 +94,5 @@
         public void onMovementBoundsChanged(boolean fromImeAdjustment) {}
 
         public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {}
-
-        public void onActivityHidden(ComponentName componentName) {}
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 3cae72d..f3a8fbf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -24,6 +24,7 @@
 import static android.view.Surface.ROTATION_270;
 import static android.view.Surface.ROTATION_90;
 import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_PIP;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
@@ -300,6 +301,10 @@
                     finishTransaction);
         }
 
+        if (isCurrentPipActivityClosed(info)) {
+            mPipBoundsState.setLastPipComponentName(null /* componentName */);
+        }
+
         return false;
     }
 
@@ -322,6 +327,21 @@
         return true;
     }
 
+    private boolean isCurrentPipActivityClosed(TransitionInfo info) {
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            TransitionInfo.Change change = info.getChanges().get(i);
+            boolean isTaskChange = change.getTaskInfo() != null;
+            boolean hasComponentNameOfPip = change.getActivityComponent() != null
+                    && change.getActivityComponent().equals(
+                            mPipBoundsState.getLastPipComponentName());
+            if (!isTaskChange && change.getMode() == TRANSIT_CLOSE && hasComponentNameOfPip) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+
     @Override
     public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 5d1b4da..448d4f5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -368,15 +368,6 @@
                     false /* fromRotation */, fromImeAdjustment, false /* fromShelfAdjustment */,
                     null /* windowContainerTransaction */);
         }
-
-        @Override
-        public void onActivityHidden(ComponentName componentName) {
-            if (componentName.equals(mPipBoundsState.getLastPipComponentName())) {
-                // The activity was removed, we don't want to restore to the reentry state
-                // saved for this component anymore.
-                mPipBoundsState.setLastPipComponentName(null);
-            }
-        }
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
index e8b01b5..7a42236 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
@@ -184,7 +184,8 @@
 
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             TransitionInfo.Change change = info.getChanges().get(i);
-            if (change == pipChange || !isOpeningMode(change.getMode())) {
+            if (change == pipChange || !isOpeningMode(change.getMode()) ||
+                    change.getTaskInfo() == null) {
                 // Ignore the change/task that's going into Pip or not opening
                 continue;
             }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/util/OWNERS
new file mode 100644
index 0000000..482aaab
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/OWNERS
@@ -0,0 +1 @@
+per-file KtProtolog.kt = file:platform/development:/tools/winscope/OWNERS
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index f9b4108..8303317 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -687,6 +687,25 @@
         verify(mRecentTasksController).onTaskRunningInfoChanged(task2);
     }
 
+    @Test
+    public void testTaskVanishedCallback() {
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN);
+        mOrganizer.onTaskAppeared(task1, /* leash= */ null);
+
+        RunningTaskInfo[] vanishedTasks = new RunningTaskInfo[1];
+        ShellTaskOrganizer.TaskVanishedListener listener =
+                new ShellTaskOrganizer.TaskVanishedListener() {
+                    @Override
+                    public void onTaskVanished(RunningTaskInfo taskInfo) {
+                        vanishedTasks[0] = taskInfo;
+                    }
+                };
+        mOrganizer.addTaskVanishedListener(listener);
+        mOrganizer.onTaskVanished(task1);
+
+        assertEquals(vanishedTasks[0], task1);
+    }
+
     private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) {
         RunningTaskInfo taskInfo = new RunningTaskInfo();
         taskInfo.taskId = taskId;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
index 731f75bf..55b6bd2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
@@ -21,10 +21,12 @@
 import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
 
 import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationRunner.calculateParentBounds;
+import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationRunner.shouldUseJumpCutForAnimation;
 import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
@@ -40,6 +42,8 @@
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
 import android.window.TransitionInfo;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -281,6 +285,18 @@
                 actualParentBounds);
     }
 
+    @Test
+    public void testShouldUseJumpCutForAnimation() {
+        final Animation noopAnimation = new AlphaAnimation(0f, 1f);
+        assertTrue("Animation without duration should use jump cut.",
+                shouldUseJumpCutForAnimation(noopAnimation));
+
+        final Animation alphaAnimation = new AlphaAnimation(0f, 1f);
+        alphaAnimation.setDuration(100);
+        assertFalse("Animation with duration should not use jump cut.",
+                shouldUseJumpCutForAnimation(alphaAnimation));
+    }
+
     @NonNull
     private static TransitionInfo.Change prepareChangeForParentBoundsCalculationTest(
             @NonNull Point endRelOffset, @NonNull Rect endAbsBounds, @NonNull Point endParentSize) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
index a64ebd3..8401264 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
@@ -76,6 +76,8 @@
     @Mock
     private ShellCommandHandler mShellCommandHandler;
     @Mock
+    private ShellTaskOrganizer mShellTaskOrganizer;
+    @Mock
     private DisplayController mDisplayController;
     @Mock
     private UiEventLogger mUiEventLogger;
@@ -96,8 +98,8 @@
     public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
         mController = new DragAndDropController(mContext, mShellInit, mShellController,
-                mShellCommandHandler, mDisplayController, mUiEventLogger, mIconProvider,
-                mGlobalDragListener, mTransitions, mMainExecutor);
+                mShellCommandHandler, mShellTaskOrganizer, mDisplayController, mUiEventLogger,
+                mIconProvider, mGlobalDragListener, mTransitions, mMainExecutor);
         mController.onInit();
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java
index 6e72e8d..582fb91 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java
@@ -65,8 +65,6 @@
 import android.graphics.Insets;
 import android.os.RemoteException;
 import android.view.DisplayInfo;
-import android.view.DragEvent;
-import android.view.View;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -76,7 +74,6 @@
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.draganddrop.DragAndDropPolicy.Target;
 import com.android.wm.shell.splitscreen.SplitScreenController;
-import com.android.wm.shell.startingsurface.TaskSnapshotWindow;
 
 import org.junit.After;
 import org.junit.Before;
@@ -106,6 +103,8 @@
     // Both the split-screen and start interface.
     @Mock
     private SplitScreenController mSplitScreenStarter;
+    @Mock
+    private DragAndDropPolicy.Starter mFullscreenStarter;
 
     @Mock
     private InstanceId mLoggerSessionId;
@@ -151,7 +150,7 @@
         mPortraitDisplayLayout = new DisplayLayout(info2, res, false, false);
         mInsets = Insets.of(0, 0, 0, 0);
 
-        mPolicy = spy(new DragAndDropPolicy(mContext, mSplitScreenStarter, mSplitScreenStarter));
+        mPolicy = spy(new DragAndDropPolicy(mContext, mSplitScreenStarter, mFullscreenStarter));
         mActivityClipData = createAppClipData(MIMETYPE_APPLICATION_ACTIVITY);
         mLaunchableIntentPendingIntent = mock(PendingIntent.class);
         when(mLaunchableIntentPendingIntent.getCreatorUserHandle())
@@ -285,13 +284,13 @@
         setRunningTask(mHomeTask);
         DragSession dragSession = new DragSession(mActivityTaskManager,
                 mLandscapeDisplayLayout, data, 0 /* dragFlags */);
-        dragSession.update();
+        dragSession.initialize();
         mPolicy.start(dragSession, mLoggerSessionId);
         ArrayList<Target> targets = assertExactTargetTypes(
                 mPolicy.getTargets(mInsets), TYPE_FULLSCREEN);
 
         mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN));
-        verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(),
+        verify(mFullscreenStarter).startIntent(any(), anyInt(), any(),
                 eq(SPLIT_POSITION_UNDEFINED), any());
     }
 
@@ -300,7 +299,7 @@
         setRunningTask(mFullscreenAppTask);
         DragSession dragSession = new DragSession(mActivityTaskManager,
                 mLandscapeDisplayLayout, data, 0 /* dragFlags */);
-        dragSession.update();
+        dragSession.initialize();
         mPolicy.start(dragSession, mLoggerSessionId);
         ArrayList<Target> targets = assertExactTargetTypes(
                 mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
@@ -320,7 +319,7 @@
         setRunningTask(mFullscreenAppTask);
         DragSession dragSession = new DragSession(mActivityTaskManager,
                 mPortraitDisplayLayout, data, 0 /* dragFlags */);
-        dragSession.update();
+        dragSession.initialize();
         mPolicy.start(dragSession, mLoggerSessionId);
         ArrayList<Target> targets = assertExactTargetTypes(
                 mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM);
@@ -340,7 +339,7 @@
         setRunningTask(mFullscreenAppTask);
         DragSession dragSession = new DragSession(mActivityTaskManager,
                 mLandscapeDisplayLayout, mActivityClipData, 0 /* dragFlags */);
-        dragSession.update();
+        dragSession.initialize();
         mPolicy.start(dragSession, mLoggerSessionId);
         ArrayList<Target> targets = mPolicy.getTargets(mInsets);
         for (Target t : targets) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index 38e741a..75d2145 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -34,7 +34,6 @@
 
 import static java.lang.Integer.MAX_VALUE;
 
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
@@ -235,27 +234,6 @@
     }
 
     @Test
-    public void onActivityHidden_isLastPipComponentName_clearLastPipComponent() {
-        final ComponentName component1 = new ComponentName(mContext, "component1");
-        when(mMockPipBoundsState.getLastPipComponentName()).thenReturn(component1);
-
-        mPipController.mPinnedTaskListener.onActivityHidden(component1);
-
-        verify(mMockPipBoundsState).setLastPipComponentName(null);
-    }
-
-    @Test
-    public void onActivityHidden_isNotLastPipComponentName_lastPipComponentNotCleared() {
-        final ComponentName component1 = new ComponentName(mContext, "component1");
-        final ComponentName component2 = new ComponentName(mContext, "component2");
-        when(mMockPipBoundsState.getLastPipComponentName()).thenReturn(component1);
-
-        mPipController.mPinnedTaskListener.onActivityHidden(component2);
-
-        verify(mMockPipBoundsState, never()).setLastPipComponentName(null);
-    }
-
-    @Test
     public void saveReentryState_savesPipBoundsState() {
         final Rect bounds = new Rect(0, 0, 10, 10);
         when(mMockPipBoundsAlgorithm.getSnapFraction(bounds)).thenReturn(1.0f);
diff --git a/media/tests/MediaFrameworkTest/Android.bp b/media/tests/MediaFrameworkTest/Android.bp
index 1325fc1..028c97e 100644
--- a/media/tests/MediaFrameworkTest/Android.bp
+++ b/media/tests/MediaFrameworkTest/Android.bp
@@ -24,6 +24,7 @@
         "flag-junit",
         "testng",
         "truth",
+        "collector-device-lib-platform",
     ],
     jni_libs: [
         "libdexmakerjvmtiagent",
diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig
index 4ac3e67..8666584 100644
--- a/packages/SettingsLib/aconfig/settingslib.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -64,13 +64,6 @@
 }
 
 flag {
-  name: "allow_all_widgets_on_lockscreen_by_default"
-  namespace: "systemui"
-  description: "Allow all widgets on the lock screen by default."
-  bug: "328261690"
-}
-
-flag {
     name: "enable_determining_advanced_details_header_with_metadata"
     namespace: "pixel_cross_device_control"
     description: "Use metadata instead of device type to determine whether a bluetooth device should use advanced details header."
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
index df0e618..8868837 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
@@ -23,19 +23,8 @@
 import android.telephony.UiccSlotInfo;
 import android.telephony.UiccSlotMapping;
 
-import java.util.List;
-
 public class DataServiceUtils {
 
-    public static <T> boolean shouldUpdateEntityList(List<T> oldList, List<T> newList) {
-        if ((oldList != null &&
-                (newList.isEmpty() || !newList.equals(oldList)))
-                || (!newList.isEmpty() && oldList == null)) {
-            return true;
-        }
-        return false;
-    }
-
     /**
      * Represents columns of the MobileNetworkInfoData table, define these columns from
      * {@see MobileNetworkUtils} or relevant common APIs.
@@ -52,73 +41,16 @@
         public static final String COLUMN_ID = "subId";
 
         /**
-         * The name of the contact discovery enabled state column,
-         * {@see MobileNetworkUtils#isContactDiscoveryEnabled(Context, int)}.
-         */
-        public static final String COLUMN_IS_CONTACT_DISCOVERY_ENABLED =
-                "isContactDiscoveryEnabled";
-
-        /**
-         * The name of the contact discovery visible state column,
-         * {@see MobileNetworkUtils#isContactDiscoveryEnabled(Context, int)}.
-         */
-        public static final String COLUMN_IS_CONTACT_DISCOVERY_VISIBLE =
-                "isContactDiscoveryVisible";
-
-        /**
          * The name of the mobile network data state column,
          * {@see MobileNetworkUtils#isMobileDataEnabled(Context)}.
          */
         public static final String COLUMN_IS_MOBILE_DATA_ENABLED = "isMobileDataEnabled";
 
         /**
-         * The name of the CDMA option state column,
-         * {@see MobileNetworkUtils#isCdmaOptions(Context, int)}.
-         */
-        public static final String COLUMN_IS_CDMA_OPTIONS = "isCdmaOptions";
-
-        /**
-         * The name of the GSM option state column,
-         * {@see MobileNetworkUtils#isGsmOptions(Context, int)}.
-         */
-        public static final String COLUMN_IS_GSM_OPTIONS = "isGsmOptions";
-
-        /**
-         * The name of the world mode state column,
-         * {@see MobileNetworkUtils#isWorldMode(Context, int)}.
-         */
-        public static final String COLUMN_IS_WORLD_MODE = "isWorldMode";
-
-        /**
-         * The name of the display network select options state column,
-         * {@see MobileNetworkUtils#shouldDisplayNetworkSelectOptions(Context, int)}.
-         */
-        public static final String COLUMN_SHOULD_DISPLAY_NETWORK_SELECT_OPTIONS =
-                "shouldDisplayNetworkSelectOptions";
-
-        /**
-         * The name of the TDSCDMA supported state column,
-         * {@see MobileNetworkUtils#isTdscdmaSupported(Context, int)}.
-         */
-        public static final String COLUMN_IS_TDSCDMA_SUPPORTED = "isTdscdmaSupported";
-
-        /**
-         * The name of the active network is cellular state column,
-         * {@see MobileNetworkUtils#activeNetworkIsCellular(Context)}.
-         */
-        public static final String COLUMN_ACTIVE_NETWORK_IS_CELLULAR = "activeNetworkIsCellular";
-
-        /**
          * The name of the show toggle for physicalSim state column,
          * {@see SubscriptionUtil#showToggleForPhysicalSim(SubscriptionManager)}.
          */
         public static final String COLUMN_SHOW_TOGGLE_FOR_PHYSICAL_SIM = "showToggleForPhysicalSim";
-
-        /**
-         * The name of the subscription's data roaming state column,
-         * {@see TelephonyManager#isDataRoamingEnabled()}.
-         */
-        public static final String COLUMN_IS_DATA_ROAMING_ENABLED = "isDataRoamingEnabled";
     }
 
     /**
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkInfoEntity.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkInfoEntity.java
index e72346d..13f99e9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkInfoEntity.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkInfoEntity.java
@@ -26,23 +26,11 @@
 @Entity(tableName = DataServiceUtils.MobileNetworkInfoData.TABLE_NAME)
 public class MobileNetworkInfoEntity {
 
-    public MobileNetworkInfoEntity(@NonNull String subId, boolean isContactDiscoveryEnabled,
-            boolean isContactDiscoveryVisible, boolean isMobileDataEnabled, boolean isCdmaOptions,
-            boolean isGsmOptions, boolean isWorldMode, boolean shouldDisplayNetworkSelectOptions,
-            boolean isTdscdmaSupported, boolean activeNetworkIsCellular,
-            boolean showToggleForPhysicalSim, boolean isDataRoamingEnabled) {
+    public MobileNetworkInfoEntity(@NonNull String subId, boolean isMobileDataEnabled,
+            boolean showToggleForPhysicalSim) {
         this.subId = subId;
-        this.isContactDiscoveryEnabled = isContactDiscoveryEnabled;
-        this.isContactDiscoveryVisible = isContactDiscoveryVisible;
         this.isMobileDataEnabled = isMobileDataEnabled;
-        this.isCdmaOptions = isCdmaOptions;
-        this.isGsmOptions = isGsmOptions;
-        this.isWorldMode = isWorldMode;
-        this.shouldDisplayNetworkSelectOptions = shouldDisplayNetworkSelectOptions;
-        this.isTdscdmaSupported = isTdscdmaSupported;
-        this.activeNetworkIsCellular = activeNetworkIsCellular;
         this.showToggleForPhysicalSim = showToggleForPhysicalSim;
-        this.isDataRoamingEnabled = isDataRoamingEnabled;
     }
 
     @PrimaryKey
@@ -50,55 +38,18 @@
     @NonNull
     public String subId;
 
-    @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_IS_CONTACT_DISCOVERY_ENABLED)
-    public boolean isContactDiscoveryEnabled;
-
-    @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_IS_CONTACT_DISCOVERY_VISIBLE)
-    public boolean isContactDiscoveryVisible;
-
     @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_IS_MOBILE_DATA_ENABLED)
     public boolean isMobileDataEnabled;
 
-    @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_IS_CDMA_OPTIONS)
-    public boolean isCdmaOptions;
-
-    @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_IS_GSM_OPTIONS)
-    public boolean isGsmOptions;
-
-    @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_IS_WORLD_MODE)
-    public boolean isWorldMode;
-
-    @ColumnInfo(name =
-            DataServiceUtils.MobileNetworkInfoData.COLUMN_SHOULD_DISPLAY_NETWORK_SELECT_OPTIONS)
-    public boolean shouldDisplayNetworkSelectOptions;
-
-    @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_IS_TDSCDMA_SUPPORTED)
-    public boolean isTdscdmaSupported;
-
-    @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_ACTIVE_NETWORK_IS_CELLULAR)
-    public boolean activeNetworkIsCellular;
-
     @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_SHOW_TOGGLE_FOR_PHYSICAL_SIM)
     public boolean showToggleForPhysicalSim;
 
-    @ColumnInfo(name = DataServiceUtils.MobileNetworkInfoData.COLUMN_IS_DATA_ROAMING_ENABLED)
-    public boolean isDataRoamingEnabled;
-
     @Override
     public int hashCode() {
         int result = 17;
         result = 31 * result + subId.hashCode();
-        result = 31 * result + Boolean.hashCode(isContactDiscoveryEnabled);
-        result = 31 * result + Boolean.hashCode(isContactDiscoveryVisible);
         result = 31 * result + Boolean.hashCode(isMobileDataEnabled);
-        result = 31 * result + Boolean.hashCode(isCdmaOptions);
-        result = 31 * result + Boolean.hashCode(isGsmOptions);
-        result = 31 * result + Boolean.hashCode(isWorldMode);
-        result = 31 * result + Boolean.hashCode(shouldDisplayNetworkSelectOptions);
-        result = 31 * result + Boolean.hashCode(isTdscdmaSupported);
-        result = 31 * result + Boolean.hashCode(activeNetworkIsCellular);
         result = 31 * result + Boolean.hashCode(showToggleForPhysicalSim);
-        result = 31 * result + Boolean.hashCode(isDataRoamingEnabled);
         return result;
     }
 
@@ -113,45 +64,18 @@
 
         MobileNetworkInfoEntity info = (MobileNetworkInfoEntity) obj;
         return  TextUtils.equals(subId, info.subId)
-                && isContactDiscoveryEnabled == info.isContactDiscoveryEnabled
-                && isContactDiscoveryVisible == info.isContactDiscoveryVisible
                 && isMobileDataEnabled == info.isMobileDataEnabled
-                && isCdmaOptions == info.isCdmaOptions
-                && isGsmOptions == info.isGsmOptions
-                && isWorldMode == info.isWorldMode
-                && shouldDisplayNetworkSelectOptions == info.shouldDisplayNetworkSelectOptions
-                && isTdscdmaSupported == info.isTdscdmaSupported
-                && activeNetworkIsCellular == info.activeNetworkIsCellular
-                && showToggleForPhysicalSim == info.showToggleForPhysicalSim
-                && isDataRoamingEnabled == info.isDataRoamingEnabled;
+                && showToggleForPhysicalSim == info.showToggleForPhysicalSim;
     }
 
     public String toString() {
         StringBuilder builder = new StringBuilder();
         builder.append(" {MobileNetworkInfoEntity(subId = ")
                 .append(subId)
-                .append(", isContactDiscoveryEnabled = ")
-                .append(isContactDiscoveryEnabled)
-                .append(", isContactDiscoveryVisible = ")
-                .append(isContactDiscoveryVisible)
                 .append(", isMobileDataEnabled = ")
                 .append(isMobileDataEnabled)
-                .append(", isCdmaOptions = ")
-                .append(isCdmaOptions)
-                .append(", isGsmOptions = ")
-                .append(isGsmOptions)
-                .append(", isWorldMode = ")
-                .append(isWorldMode)
-                .append(", shouldDisplayNetworkSelectOptions = ")
-                .append(shouldDisplayNetworkSelectOptions)
-                .append(", isTdscdmaSupported = ")
-                .append(isTdscdmaSupported)
                 .append(", activeNetworkIsCellular = ")
-                .append(activeNetworkIsCellular)
-                .append(", showToggleForPhysicalSim = ")
                 .append(showToggleForPhysicalSim)
-                .append(", isDataRoamingEnabled = ")
-                .append(isDataRoamingEnabled)
                 .append(")}");
         return builder.toString();
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt
index 727c61c..a7e0464 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.kt
@@ -498,7 +498,7 @@
         ): Job =
             coroutineScope.launch {
                 val wifiManager = context.getSystemService(WifiManager::class.java) ?: return@launch
-                if (wifiManager.queryWepAllowed()) {
+                if (wifiManager.isWepSupported == true && wifiManager.queryWepAllowed()) {
                     onAllowed()
                 } else {
                     val intent = Intent(Intent.ACTION_MAIN).apply {
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index 05eb044..861c405 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -203,6 +203,12 @@
 
     private static final String NULL_VALUE = "null";
 
+    // TOBO(b/312444587): remove after Test Mission 2.
+    // Bulk sync names
+    private static final String BULK_SYNC_MARKER = "aconfigd_marker/bulk_synced";
+    private static final String BULK_SYNC_TRIGGER_COUNTER =
+        "core_experiments_team_internal/BulkSyncTriggerCounterFlag__bulk_sync_trigger_counter";
+
     private static final ArraySet<String> sSystemPackages = new ArraySet<>();
 
     private final Object mWriteLock = new Object();
@@ -409,8 +415,7 @@
                     }
                 }
                 // TOBO(b/312444587): remove the comparison logic after Test Mission 2.
-                if (mSettings.get("aconfigd_marker/bulk_synced").value.equals("true")
-                        && requests == null) {
+                if (requests == null) {
                     Map<String, AconfigdFlagInfo> aconfigdFlagMap =
                             AconfigdJavaUtils.listFlagsValueInNewStorage(localSocket);
                     compareFlagValueInNewStorage(
@@ -534,7 +539,7 @@
             return null;
         }
         AconfigdFlagInfo flag = flagInfoDefault.get(fullFlagName);
-        if (flag == null) {
+        if (flag == null || !namespace.equals(flag.getNamespace())) {
             return null;
         }
 
@@ -553,15 +558,33 @@
     public ProtoOutputStream handleBulkSyncToNewStorage(
             Map<String, AconfigdFlagInfo> aconfigFlagMap) {
         // get marker or add marker if it does not exist
-        final String bulkSyncMarkerName = new String("aconfigd_marker/bulk_synced");
-        Setting markerSetting = mSettings.get(bulkSyncMarkerName);
+        Setting markerSetting = mSettings.get(BULK_SYNC_MARKER);
+        int localCounter = 0;
         if (markerSetting == null) {
-            markerSetting = new Setting(bulkSyncMarkerName, "false", false, "aconfig", "aconfig");
-            mSettings.put(bulkSyncMarkerName, markerSetting);
+            markerSetting = new Setting(BULK_SYNC_MARKER, "0", false, "aconfig", "aconfig");
+            mSettings.put(BULK_SYNC_MARKER, markerSetting);
+        }
+        try {
+            localCounter = Integer.parseInt(markerSetting.value);
+        } catch(NumberFormatException e) {
+            // reset local counter
+            markerSetting.value = "0";
         }
 
         if (enableAconfigStorageDaemon()) {
-            if (markerSetting.value.equals("true")) {
+            Setting bulkSyncCounter = mSettings.get(BULK_SYNC_TRIGGER_COUNTER);
+            int serverCounter = 0;
+            if (bulkSyncCounter != null) {
+                try {
+                    serverCounter = Integer.parseInt(bulkSyncCounter.value);
+                } catch (NumberFormatException e) {
+                    // reset the local value of server counter
+                    bulkSyncCounter.value = "0";
+                }
+            }
+
+            boolean shouldSync = localCounter < serverCounter;
+            if (!shouldSync) {
                 // CASE 1, flag is on, bulk sync marker true, nothing to do
                 return null;
             } else {
@@ -600,20 +623,12 @@
                 }
 
                 // mark sync has been done
-                markerSetting.value = "true";
+                markerSetting.value = String.valueOf(serverCounter);
                 scheduleWriteIfNeededLocked();
                 return requests;
             }
         } else {
-            if (markerSetting.value.equals("true")) {
-                // CASE 3, flag is off, bulk sync marker true, clear the marker
-                markerSetting.value = "false";
-                scheduleWriteIfNeededLocked();
-                return null;
-            } else {
-                // CASE 4, flag is off, bulk sync marker false, nothing to do
-                return null;
-            }
+            return null;
         }
     }
 
@@ -692,6 +707,7 @@
                                     .setFlagName(flag.getName())
                                     .setDefaultFlagValue(flagValue)
                                     .setIsReadWrite(isReadWrite)
+                                    .setNamespace(flag.getNamespace())
                                     .build());
                 }
             }
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
index 94aeb9b..4b4ced3 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -151,12 +151,14 @@
                                                 .setFlagName("flag1")
                                                 .setDefaultFlagValue("false")
                                                 .setIsReadWrite(true)
+                                                .setNamespace("test_namespace")
                                                 .build();
         AconfigdFlagInfo flag2 = AconfigdFlagInfo.newBuilder()
                                                 .setPackageName("com.android.flags")
                                                 .setFlagName("flag2")
                                                 .setDefaultFlagValue("true")
                                                 .setIsReadWrite(false)
+                                                .setNamespace("test_namespace")
                                                 .build();
         Map<String, AconfigdFlagInfo> flagInfoDefault = new HashMap<>();
 
@@ -1018,12 +1020,17 @@
                         .setFlagName("flag1")
                         .setDefaultFlagValue("false")
                         .setIsReadWrite(true)
+                        .setNamespace("test_namespace")
                         .build();
 
         flagInfoDefault.put(flag1.getFullFlagName(), flag1);
 
-        // server override
+        // not the right namespace
+        assertNull(
+                settingsState.getFlagOverrideToSync(
+                        "some_namespace/com.android.flags.flag1", "true", flagInfoDefault));
 
+        // server override
         settingsState.getFlagOverrideToSync(
                 "test_namespace/com.android.flags.flag1", "true", flagInfoDefault);
         assertEquals("com.android.flags", flag1.getPackageName());
@@ -1079,21 +1086,45 @@
                         .setIsReadWrite(false)
                         .build());
 
+        String bulkSyncMarker = "aconfigd_marker/bulk_synced";
+        String bulkSyncCounter =
+                "core_experiments_team_internal/" +
+                "BulkSyncTriggerCounterFlag__bulk_sync_trigger_counter";
+
         synchronized (lock) {
-            settingsState.insertSettingLocked(
-                    "aconfigd_marker/bulk_synced", "false", null, false, "aconfig");
+            settingsState.insertSettingLocked(bulkSyncMarker, "0", null, false, "aconfig");
+            settingsState.insertSettingLocked(bulkSyncCounter, "1", null, false,
+                    "com.google.android.platform.core_experiments_team_internal");
 
             // first bulk sync
             ProtoOutputStream requests = settingsState.handleBulkSyncToNewStorage(flags);
             assertTrue(requests != null);
             String value = settingsState.getSettingLocked("aconfigd_marker/bulk_synced").getValue();
-            assertEquals("true", value);
+            assertEquals("1", value);
 
             // send time should no longer bulk sync
             requests = settingsState.handleBulkSyncToNewStorage(flags);
-            assertTrue(requests == null);
+            assertNull(requests);
             value = settingsState.getSettingLocked("aconfigd_marker/bulk_synced").getValue();
-            assertEquals("true", value);
+            assertEquals("1", value);
+
+            // won't sync if the marker is string
+            settingsState.insertSettingLocked(bulkSyncMarker, "true", null, false, "aconfig");
+            settingsState.insertSettingLocked(bulkSyncCounter, "0", null, false,
+                    "com.google.android.platform.core_experiments_team_internal");
+            requests = settingsState.handleBulkSyncToNewStorage(flags);
+            assertNull(requests);
+            value = settingsState.getSettingLocked("aconfigd_marker/bulk_synced").getValue();
+            assertEquals("0", value);
+
+            // won't sync if the marker and counter value are the same
+            settingsState.insertSettingLocked(bulkSyncMarker, "1", null, false, "aconfig");
+            settingsState.insertSettingLocked(bulkSyncCounter, "1", null, false,
+                    "com.google.android.platform.core_experiments_team_internal");
+            requests = settingsState.handleBulkSyncToNewStorage(flags);
+            assertNull(requests);
+            value = settingsState.getSettingLocked("aconfigd_marker/bulk_synced").getValue();
+            assertEquals("1", value);
         }
     }
 
@@ -1107,21 +1138,34 @@
                 SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
 
         Map<String, AconfigdFlagInfo> flags = new HashMap<>();
+        String bulkSyncMarker = "aconfigd_marker/bulk_synced";
+        String bulkSyncCounter =
+                "core_experiments_team_internal/" +
+                "BulkSyncTriggerCounterFlag__bulk_sync_trigger_counter";
         synchronized (lock) {
             settingsState.insertSettingLocked("aconfigd_marker/bulk_synced",
                     "true", null, false, "aconfig");
 
             // when aconfigd is off, should change the marker to false
             ProtoOutputStream requests = settingsState.handleBulkSyncToNewStorage(flags);
-            assertTrue(requests == null);
+            assertNull(requests);
             String value = settingsState.getSettingLocked("aconfigd_marker/bulk_synced").getValue();
-            assertEquals("false", value);
+            assertEquals("0", value);
 
             // marker started with false value, after call, it should remain false
             requests = settingsState.handleBulkSyncToNewStorage(flags);
-            assertTrue(requests == null);
+            assertNull(requests);
             value = settingsState.getSettingLocked("aconfigd_marker/bulk_synced").getValue();
-            assertEquals("false", value);
+            assertEquals("0", value);
+
+            // won't sync
+            settingsState.insertSettingLocked(bulkSyncMarker, "0", null, false, "aconfig");
+            settingsState.insertSettingLocked(bulkSyncCounter, "1", null, false,
+                    "com.google.android.platform.core_experiments_team_internal");
+            requests = settingsState.handleBulkSyncToNewStorage(flags);
+            assertNull(requests);
+            value = settingsState.getSettingLocked("aconfigd_marker/bulk_synced").getValue();
+            assertEquals("0", value);
         }
     }
 
@@ -1164,6 +1208,7 @@
                         .setFlagName("flag1")
                         .setDefaultFlagValue("false")
                         .setIsReadWrite(true)
+                        .setNamespace("test_namespace")
                         .build();
         flagInfoDefault.put(flag1.getFullFlagName(), flag1);
 
@@ -1186,6 +1231,7 @@
                         .setFlagName("flag2")
                         .setDefaultFlagValue("false")
                         .setIsReadWrite(true)
+                        .setNamespace("test_namespace")
                         .build();
         flagInfoDefault.put(flag2.getFullFlagName(), flag2);
         synchronized (lock) {
@@ -1207,6 +1253,7 @@
                         .setFlagName("flag3")
                         .setDefaultFlagValue("false")
                         .setIsReadWrite(false)
+                        .setNamespace("test_namespace")
                         .build();
         flagInfoDefault.put(flag3.getFullFlagName(), flag3);
         synchronized (lock) {
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index b37db16..1b9a09d 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -373,6 +373,8 @@
     <!-- Listen to (dis-)connection of external displays and enable / disable them. -->
     <uses-permission android:name="android.permission.MANAGE_DISPLAYS" />
 
+    <uses-permission android:name="android.permission.SET_BIOMETRIC_DIALOG_ADVANCED" />
+
     <protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" />
     <protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" />
     <protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" />
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 4bd8b3b..f5153e1 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1032,13 +1032,6 @@
 }
 
 flag {
-  name: "glanceable_hub_shortcut_button"
-  namespace: "systemui"
-  description: "Shows a button over the dream and lock screen to open the glanceable hub"
-  bug: "339667383"
-}
-
-flag {
   name: "glanceable_hub_gesture_handle"
   namespace: "systemui"
   description: "Shows a vertical bar at the right edge to indicate the user can swipe to open the glanceable hub"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 9c2127c..be51c1a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -484,6 +484,7 @@
             rememberDragAndDropTargetState(
                 gridState = gridState,
                 contentListState = contentListState,
+                contentOffset = contentOffset,
                 updateDragPositionForRemove = updateDragPositionForRemove
             )
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
index 37fe798..9e6f22a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
@@ -41,7 +41,7 @@
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.dp
 import com.android.systemui.communal.domain.model.CommunalContentModel
-import com.android.systemui.communal.ui.compose.extensions.plus
+import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset
 import com.android.systemui.communal.util.WidgetPickerIntentUtils
 import com.android.systemui.communal.util.WidgetPickerIntentUtils.getWidgetExtraFromIntent
 import kotlinx.coroutines.CoroutineScope
@@ -57,6 +57,7 @@
 @Composable
 internal fun rememberDragAndDropTargetState(
     gridState: LazyGridState,
+    contentOffset: Offset,
     contentListState: ContentListState,
     updateDragPositionForRemove: (offset: Offset) -> Boolean,
 ): DragAndDropTargetState {
@@ -70,6 +71,7 @@
         remember(gridState, contentListState) {
             DragAndDropTargetState(
                 state = gridState,
+                contentOffset = contentOffset,
                 contentListState = contentListState,
                 scope = scope,
                 autoScrollSpeed = autoScrollSpeed,
@@ -145,6 +147,7 @@
  */
 internal class DragAndDropTargetState(
     private val state: LazyGridState,
+    private val contentOffset: Offset,
     private val contentListState: ContentListState,
     private val scope: CoroutineScope,
     private val autoScrollSpeed: MutableState<Float>,
@@ -214,8 +217,7 @@
                 return@let true
             }
             return false
-        }
-            ?: false
+        } ?: false
     }
 
     fun onEnded() {
@@ -249,10 +251,9 @@
     }
 
     private fun findTargetItem(dragEvent: DragEvent): LazyGridItemInfo? =
-        state.layoutInfo.visibleItemsInfo.firstOrNull { item ->
-            dragEvent.x.toInt() in item.offset.x..(item.offset + item.size).x &&
-                dragEvent.y.toInt() in item.offset.y..(item.offset + item.size).y
-        }
+        state.layoutInfo.visibleItemsInfo.firstItemAtOffset(
+            Offset(dragEvent.x, dragEvent.y) - contentOffset
+        )
 
     private fun movePlaceholderTo(index: Int) {
         val currentIndex = contentListState.list.indexOf(placeHolder)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index 22566e7..9c9e6c6 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -34,6 +34,8 @@
 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayout
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
 import com.android.compose.animation.scene.observableTransitionState
 import com.android.systemui.ribbon.ui.composable.BottomRightCornerRibbon
 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
@@ -56,7 +58,6 @@
  *   must have entries in this map.
  * @param modifier A modifier.
  */
-@OptIn(ExperimentalComposeUiApi::class)
 @Composable
 fun SceneContainer(
     viewModel: SceneContainerViewModel,
@@ -66,8 +67,6 @@
 ) {
     val coroutineScope = rememberCoroutineScope()
     val currentSceneKey: SceneKey by viewModel.currentScene.collectAsStateWithLifecycle()
-    val currentDestinations by
-        viewModel.currentDestinationScenes(coroutineScope).collectAsStateWithLifecycle()
     val state: MutableSceneTransitionLayoutState = remember {
         MutableSceneTransitionLayoutState(
             initialScene = currentSceneKey,
@@ -88,20 +87,19 @@
         onDispose { viewModel.setTransitionState(null) }
     }
 
+    val userActionsBySceneKey: Map<SceneKey, Map<UserAction, UserActionResult>> =
+        sceneByKey.values.associate { scene ->
+            val userActions by scene.destinationScenes.collectAsStateWithLifecycle(emptyMap())
+            val resolvedUserActions = viewModel.resolveSceneFamilies(userActions)
+            scene.key to resolvedUserActions
+        }
+
     Box(
         modifier = Modifier.fillMaxSize(),
     ) {
         SceneTransitionLayout(state = state, modifier = modifier.fillMaxSize()) {
             sceneByKey.forEach { (sceneKey, composableScene) ->
-                scene(
-                    key = sceneKey,
-                    userActions =
-                        if (sceneKey == currentSceneKey) {
-                            currentDestinations
-                        } else {
-                            viewModel.resolveSceneFamilies(composableScene.destinationScenes.value)
-                        },
-                ) {
+                scene(key = sceneKey, userActions = checkNotNull(userActionsBySceneKey[sceneKey])) {
                     with(composableScene) {
                         this@scene.Content(
                             modifier = Modifier.element(sceneKey.rootElementKey).fillMaxSize(),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt
index fb2b33d..da40f64 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt
@@ -20,7 +20,6 @@
 import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE
 import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL
 import android.app.admin.devicePolicyManager
-import android.appwidget.AppWidgetProviderInfo
 import android.content.Intent
 import android.content.pm.UserInfo
 import android.os.UserManager.USER_TYPE_PROFILE_MANAGED
@@ -29,7 +28,6 @@
 import android.provider.Settings
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.settingslib.flags.Flags.FLAG_ALLOW_ALL_WIDGETS_ON_LOCKSCREEN_BY_DEFAULT
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.broadcastDispatcher
@@ -183,42 +181,6 @@
                 )
         }
 
-    @EnableFlags(FLAG_COMMUNAL_HUB)
-    @Test
-    fun hubShowsWidgetCategoriesSetByUser() =
-        testScope.runTest {
-            kosmos.fakeSettings.putIntForUser(
-                CommunalSettingsRepositoryImpl.GLANCEABLE_HUB_CONTENT_SETTING,
-                AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN,
-                PRIMARY_USER.id
-            )
-            val setting by collectLastValue(underTest.getWidgetCategories(PRIMARY_USER))
-            assertThat(setting?.categories)
-                .isEqualTo(AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN)
-        }
-
-    @EnableFlags(FLAG_COMMUNAL_HUB)
-    @DisableFlags(FLAG_ALLOW_ALL_WIDGETS_ON_LOCKSCREEN_BY_DEFAULT)
-    @Test
-    fun hubShowsKeyguardWidgetsByDefault() =
-        testScope.runTest {
-            val setting by collectLastValue(underTest.getWidgetCategories(PRIMARY_USER))
-            assertThat(setting?.categories)
-                .isEqualTo(AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD)
-        }
-
-    @EnableFlags(FLAG_COMMUNAL_HUB, FLAG_ALLOW_ALL_WIDGETS_ON_LOCKSCREEN_BY_DEFAULT)
-    @Test
-    fun hubShowsAllWidgetsByDefaultWhenFlagEnabled() =
-        testScope.runTest {
-            val setting by collectLastValue(underTest.getWidgetCategories(PRIMARY_USER))
-            assertThat(setting?.categories)
-                .isEqualTo(
-                    AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD +
-                        AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
-                )
-        }
-
     @Test
     fun backgroundType_defaultValue() =
         testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index d951cca..7b26db5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -36,7 +36,6 @@
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.broadcastDispatcher
-import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryImpl
 import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
 import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository
 import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
@@ -81,7 +80,6 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.settings.fakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -915,14 +913,6 @@
             )
             runCurrent()
 
-            // Keyguard widgets are allowed.
-            kosmos.fakeSettings.putIntForUser(
-                CommunalSettingsRepositoryImpl.GLANCEABLE_HUB_CONTENT_SETTING,
-                AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD,
-                mainUser.id
-            )
-            runCurrent()
-
             // When work profile is paused.
             whenever(userManager.isQuietModeEnabled(eq(UserHandle.of(USER_INFO_WORK.id))))
                 .thenReturn(true)
@@ -956,93 +946,6 @@
         }
 
     @Test
-    fun widgetContent_containsDisabledWidgets_whenCategoryNotAllowed() =
-        testScope.runTest {
-            // Communal available, and tutorial completed.
-            keyguardRepository.setKeyguardShowing(true)
-            keyguardRepository.setKeyguardOccluded(false)
-            tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
-
-            val userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK)
-            userRepository.setUserInfos(userInfos)
-            userTracker.set(
-                userInfos = userInfos,
-                selectedUserIndex = 0,
-            )
-            userRepository.setSelectedUserInfo(MAIN_USER_INFO)
-            runCurrent()
-
-            // Widgets available.
-            val widget1 =
-                createWidgetWithCategory(1, AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN)
-            val widget2 =
-                createWidgetWithCategory(2, AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD)
-            val widget3 =
-                createWidgetWithCategory(3, AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX)
-            val widgets = listOf(widget1, widget2, widget3)
-            widgetRepository.setCommunalWidgets(widgets)
-
-            val widgetContent by collectLastValue(underTest.widgetContent)
-            kosmos.fakeSettings.putIntForUser(
-                CommunalSettingsRepositoryImpl.GLANCEABLE_HUB_CONTENT_SETTING,
-                AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD,
-                mainUser.id
-            )
-
-            // Only the keyguard widget is enabled.
-            assertThat(widgetContent).hasSize(3)
-            assertThat(widgetContent!!.get(0))
-                .isInstanceOf(CommunalContentModel.WidgetContent.DisabledWidget::class.java)
-            assertThat(widgetContent!!.get(1))
-                .isInstanceOf(CommunalContentModel.WidgetContent.Widget::class.java)
-            assertThat(widgetContent!!.get(2))
-                .isInstanceOf(CommunalContentModel.WidgetContent.DisabledWidget::class.java)
-        }
-
-    @Test
-    fun widgetContent_allEnabled_whenCategoryAllowed() =
-        testScope.runTest {
-            // Communal available, and tutorial completed.
-            keyguardRepository.setKeyguardShowing(true)
-            keyguardRepository.setKeyguardOccluded(false)
-            tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
-
-            val userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK)
-            userRepository.setUserInfos(userInfos)
-            userTracker.set(
-                userInfos = userInfos,
-                selectedUserIndex = 0,
-            )
-            userRepository.setSelectedUserInfo(MAIN_USER_INFO)
-            runCurrent()
-
-            // Widgets available.
-            val widget1 =
-                createWidgetWithCategory(1, AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN)
-            val widget2 =
-                createWidgetWithCategory(2, AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD)
-            val widget3 =
-                createWidgetWithCategory(3, AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD)
-            val widgets = listOf(widget1, widget2, widget3)
-            widgetRepository.setCommunalWidgets(widgets)
-
-            val widgetContent by collectLastValue(underTest.widgetContent)
-            kosmos.fakeSettings.putIntForUser(
-                CommunalSettingsRepositoryImpl.GLANCEABLE_HUB_CONTENT_SETTING,
-                AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD or
-                    AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN,
-                mainUser.id
-            )
-
-            // All widgets are enabled.
-            assertThat(widgetContent).hasSize(3)
-            widgetContent!!.forEach { model ->
-                assertThat(model)
-                    .isInstanceOf(CommunalContentModel.WidgetContent.Widget::class.java)
-            }
-        }
-
-    @Test
     fun filterWidgets_whenDisallowedByDevicePolicyForWorkProfile() =
         testScope.runTest {
             // Keyguard showing, and tutorial completed.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
index 7b7d03b..7044895 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
@@ -72,6 +72,7 @@
                 /* animationController = */ notNull(),
                 /* fillInIntent = */ refEq(fillInIntent),
                 /* extraOptions = */ refEq(activityOptions.toBundle()),
+                /* customMessage */ isNull(),
             )
     }
 
@@ -93,6 +94,7 @@
                 /* animationController = */ isNull(),
                 /* fillInIntent = */ refEq(fillInIntent),
                 /* extraOptions = */ refEq(activityOptions.toBundle()),
+                /* customMessage */ isNull(),
             )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
index 49d0399..26fcb23 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
@@ -90,7 +90,6 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = FakeUserTracker(),
-                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
         settings = FakeSettings()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt
index 9ab1ac1..0f3e78b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt
@@ -29,7 +29,6 @@
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.settings.FakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -81,7 +80,6 @@
                 context = context,
                 userFileManager = userFileManager,
                 userTracker = userTracker,
-                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
index 159ce36..8e109b4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
@@ -91,7 +91,6 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
-                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
         client1 = FakeCustomizationProviderClient()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
index 78a1167..2d77f4f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
@@ -148,7 +148,6 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
-                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
         val remoteUserSelectionManager =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt
new file mode 100644
index 0000000..1c3021e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.qs.panels.ui.compose
+
+import androidx.compose.runtime.mutableStateOf
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DragAndDropStateTest : SysuiTestCase() {
+    private val listState = EditTileListState(TestEditTiles)
+    private val underTest = DragAndDropState(mutableStateOf(null), listState)
+
+    @Test
+    fun isMoving_returnsCorrectValue() {
+        // Asserts no tiles is moving
+        TestEditTiles.forEach { assertThat(underTest.isMoving(it.tileSpec)).isFalse() }
+
+        // Start the drag movement
+        val movingTileSpec = TestEditTiles[0].tileSpec
+        underTest.onStarted(movingTileSpec)
+
+        // Assert that the correct tile is marked as moving
+        TestEditTiles.forEach {
+            assertThat(underTest.isMoving(it.tileSpec)).isEqualTo(movingTileSpec == it.tileSpec)
+        }
+    }
+
+    @Test
+    fun onMoved_updatesList() {
+        val movingTileSpec = TestEditTiles[0].tileSpec
+
+        // Start the drag movement
+        underTest.onStarted(movingTileSpec)
+
+        // Move the tile to the end of the list
+        underTest.onMoved(listState.tiles[5].tileSpec)
+        assertThat(underTest.currentPosition()).isEqualTo(5)
+
+        // Move the tile to the middle of the list
+        underTest.onMoved(listState.tiles[2].tileSpec)
+        assertThat(underTest.currentPosition()).isEqualTo(2)
+    }
+
+    @Test
+    fun onDrop_resetsMovingTile() {
+        val movingTileSpec = TestEditTiles[0].tileSpec
+
+        // Start the drag movement
+        underTest.onStarted(movingTileSpec)
+
+        // Move the tile to the end of the list
+        underTest.onMoved(listState.tiles[5].tileSpec)
+
+        // Drop the tile
+        underTest.onDrop()
+
+        // Asserts no tiles is moving
+        TestEditTiles.forEach { assertThat(underTest.isMoving(it.tileSpec)).isFalse() }
+    }
+
+    companion object {
+        private fun createEditTile(tileSpec: String): EditTileViewModel {
+            return EditTileViewModel(
+                tileSpec = TileSpec.create(tileSpec),
+                icon = Icon.Resource(0, null),
+                label = Text.Loaded("unused"),
+                appName = null,
+                isCurrent = true,
+                availableEditActions = emptySet(),
+            )
+        }
+
+        private val TestEditTiles =
+            listOf(
+                createEditTile("tileA"),
+                createEditTile("tileB"),
+                createEditTile("tileC"),
+                createEditTile("tileD"),
+                createEditTile("tileE"),
+                createEditTile("tileF"),
+            )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
new file mode 100644
index 0000000..517b601
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.qs.panels.ui.compose
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class EditTileListStateTest : SysuiTestCase() {
+    val underTest = EditTileListState(TestEditTiles)
+
+    @Test
+    fun movingNonExistentTile_listUnchanged() {
+        underTest.move(TileSpec.create("other_tile"), TestEditTiles[0].tileSpec)
+
+        assertThat(underTest.tiles).containsExactly(*TestEditTiles.toTypedArray())
+    }
+
+    @Test
+    fun movingTileToNonExistentTarget_listUnchanged() {
+        underTest.move(TestEditTiles[0].tileSpec, TileSpec.create("other_tile"))
+
+        assertThat(underTest.tiles).containsExactly(*TestEditTiles.toTypedArray())
+    }
+
+    @Test
+    fun movingTileToItself_listUnchanged() {
+        underTest.move(TestEditTiles[0].tileSpec, TestEditTiles[0].tileSpec)
+
+        assertThat(underTest.tiles).containsExactly(*TestEditTiles.toTypedArray())
+    }
+
+    @Test
+    fun movingTileToSameSection_listUpdates() {
+        // Move tile at index 0 to index 1. Tile 0 should remain current.
+        underTest.move(TestEditTiles[0].tileSpec, TestEditTiles[1].tileSpec)
+
+        // Assert the tiles 0 and 1 have changed places.
+        assertThat(underTest.tiles[0]).isEqualTo(TestEditTiles[1])
+        assertThat(underTest.tiles[1]).isEqualTo(TestEditTiles[0])
+
+        // Assert the rest of the list is unchanged
+        assertThat(underTest.tiles.subList(2, 5))
+            .containsExactly(*TestEditTiles.subList(2, 5).toTypedArray())
+    }
+
+    @Test
+    fun movingTileToDifferentSection_listAndTileUpdates() {
+        // Move tile at index 0 to index 3. Tile 0 should no longer be current.
+        underTest.move(TestEditTiles[0].tileSpec, TestEditTiles[3].tileSpec)
+
+        // Assert tile 0 is now at index 3 and is no longer current.
+        assertThat(underTest.tiles[3]).isEqualTo(TestEditTiles[0].copy(isCurrent = false))
+
+        // Assert previous tiles have shifted places
+        assertThat(underTest.tiles[0]).isEqualTo(TestEditTiles[1])
+        assertThat(underTest.tiles[1]).isEqualTo(TestEditTiles[2])
+        assertThat(underTest.tiles[2]).isEqualTo(TestEditTiles[3])
+
+        // Assert the rest of the list is unchanged
+        assertThat(underTest.tiles.subList(4, 5))
+            .containsExactly(*TestEditTiles.subList(4, 5).toTypedArray())
+    }
+
+    companion object {
+        private fun createEditTile(tileSpec: String, isCurrent: Boolean): EditTileViewModel {
+            return EditTileViewModel(
+                tileSpec = TileSpec.create(tileSpec),
+                icon = Icon.Resource(0, null),
+                label = Text.Loaded("unused"),
+                appName = null,
+                isCurrent = isCurrent,
+                availableEditActions = emptySet(),
+            )
+        }
+
+        private val TestEditTiles =
+            listOf(
+                createEditTile("tileA", true),
+                createEditTile("tileB", true),
+                createEditTile("tileC", true),
+                createEditTile("tileD", false),
+                createEditTile("tileE", false),
+                createEditTile("tileF", false),
+            )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index cb4d96f..39b3662 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -133,7 +133,6 @@
                 sceneInteractor = sceneInteractor,
                 falsingInteractor = kosmos.falsingInteractor,
                 powerInteractor = kosmos.powerInteractor,
-                scenes = kosmos.scenes,
             )
             .apply { setTransitionState(transitionState) }
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index 5c30379..ea95aab 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -28,10 +28,8 @@
 import com.android.systemui.power.data.repository.fakePowerRepository
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
-import com.android.systemui.scene.fakeScenes
 import com.android.systemui.scene.sceneContainerConfig
 import com.android.systemui.scene.sceneKeys
-import com.android.systemui.scene.scenes
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
 import com.android.systemui.testKosmos
@@ -66,7 +64,6 @@
                 sceneInteractor = sceneInteractor,
                 falsingInteractor = kosmos.falsingInteractor,
                 powerInteractor = kosmos.powerInteractor,
-                scenes = kosmos.scenes,
             )
     }
 
@@ -217,23 +214,4 @@
 
             assertThat(isVisible).isFalse()
         }
-
-    @Test
-    fun currentDestinationScenes_onlyTheCurrentSceneIsCollected() =
-        testScope.runTest {
-            val unused by collectLastValue(underTest.currentDestinationScenes(backgroundScope))
-            val currentScene by collectLastValue(sceneInteractor.currentScene)
-            kosmos.fakeScenes.forEach { scene ->
-                fakeSceneDataSource.changeScene(toScene = scene.key)
-                runCurrent()
-                assertThat(currentScene).isEqualTo(scene.key)
-
-                assertThat(scene.isDestinationScenesBeingCollected).isTrue()
-                kosmos.fakeScenes
-                    .filter { it.key != scene.key }
-                    .forEach { otherScene ->
-                        assertThat(otherScene.isDestinationScenesBeingCollected).isFalse()
-                    }
-            }
-        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
index 1f47542..23b28e3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
@@ -173,6 +173,22 @@
         }
 
     @Test
+    fun shadeExpansion_idleOnQs() =
+        testScope.runTest {
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(currentScene = Scenes.QuickSettings)
+                )
+            sceneInteractor.setTransitionState(transitionState)
+            val expandFraction by collectLastValue(scrollViewModel.expandFraction)
+            assertThat(expandFraction).isEqualTo(1f)
+
+            fakeSceneDataSource.changeScene(toScene = Scenes.QuickSettings)
+            val isScrollable by collectLastValue(scrollViewModel.isScrollable)
+            assertThat(isScrollable).isFalse()
+        }
+
+    @Test
     fun shadeExpansion_shadeToQs() =
         testScope.runTest {
             val transitionState =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
index 5887f90..ccd78ee 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
@@ -154,6 +154,25 @@
     }
 
     @Test
+    fun startPendingIntentDismissingKeyguard_withCustomMessage_dismissWithAction() {
+        val pendingIntent = mock(PendingIntent::class.java)
+        `when`(pendingIntent.isActivity).thenReturn(true)
+        `when`(keyguardStateController.isShowing).thenReturn(true)
+        `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+        val customMessage = "Custom unlock reason"
+
+        underTest.startPendingIntentDismissingKeyguard(
+            intent = pendingIntent,
+            dismissShade = true,
+            customMessage = customMessage
+        )
+        mainExecutor.runAllReady()
+
+        verify(statusBarKeyguardViewManager)
+            .dismissWithAction(any(), eq(null), anyBoolean(), eq(customMessage))
+    }
+
+    @Test
     fun startPendingIntentMaybeDismissingKeyguard_keyguardShowing_showOverLs_launchAnimator() {
         val pendingIntent = mock(PendingIntent::class.java)
         val parent = FrameLayout(context)
@@ -466,6 +485,7 @@
         animationController: ActivityTransitionAnimator.Controller?,
         fillInIntent: Intent? = null,
         extraOptions: Bundle? = null,
+        customMessage: String? = null,
     ) {
         underTest.startPendingIntentDismissingKeyguard(
             intent = intent,
@@ -475,6 +495,7 @@
             showOverLockscreen = true,
             fillInIntent = fillInIntent,
             extraOptions = extraOptions,
+            customMessage = customMessage,
         )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java
index 200e92e..7346323 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java
@@ -26,6 +26,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.os.Handler;
 import android.platform.test.flag.junit.FlagsParameterization;
 import android.testing.TestableLooper;
 
@@ -85,6 +86,8 @@
     @Mock private DumpManager dumpManager;
     private AvalancheController mAvalancheController;
 
+    @Mock private Handler mBgHandler;
+
     private static final class TestableHeadsUpManagerPhone extends HeadsUpManagerPhone {
         TestableHeadsUpManagerPhone(
                 Context context,
@@ -101,7 +104,8 @@
                 UiEventLogger uiEventLogger,
                 JavaAdapter javaAdapter,
                 ShadeInteractor shadeInteractor,
-                AvalancheController avalancheController
+                AvalancheController avalancheController,
+                Handler bgHandler
         ) {
             super(
                     context,
@@ -119,7 +123,8 @@
                     uiEventLogger,
                     javaAdapter,
                     shadeInteractor,
-                    avalancheController
+                    avalancheController,
+                    bgHandler
             );
             mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME;
             mAutoDismissTime = TEST_AUTO_DISMISS_TIME;
@@ -142,7 +147,8 @@
                 mUiEventLogger,
                 mJavaAdapter,
                 mShadeInteractor,
-                mAvalancheController
+                mAvalancheController,
+                mBgHandler
         );
     }
 
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
index 7cf56aa..abb721a 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
@@ -84,14 +84,17 @@
      * Similar to {@link #startPendingIntentMaybeDismissingKeyguard(PendingIntent, Runnable,
      * ActivityTransitionAnimator.Controller)}, but also specifies a fill-in intent and extra
      * option that could be used to populate the pending intent and launch the activity. This also
-     * allows the caller to avoid dismissing the shade.
+     * allows the caller to avoid dismissing the shade. An optional custom message can be set as
+     * the unlock reason in the alternate bouncer.
      */
     void startPendingIntentMaybeDismissingKeyguard(PendingIntent intent,
             boolean dismissShade,
             @Nullable Runnable intentSentUiThreadCallback,
             @Nullable ActivityTransitionAnimator.Controller animationController,
             @Nullable Intent fillInIntent,
-            @Nullable Bundle extraOptions);
+            @Nullable Bundle extraOptions,
+            @Nullable String customMessage
+        );
 
     /**
      * The intent flag can be specified in startActivity().
@@ -134,14 +137,20 @@
     void dismissKeyguardThenExecute(OnDismissAction action, @Nullable Runnable cancel,
             boolean afterKeyguardGone);
 
-    /** Authenticates if needed and dismisses keyguard to execute an action. */
+    /**
+     * Authenticates if needed and dismisses keyguard to execute an action.
+     *
+     * TODO(b/348431835) Display the custom message in the new alternate bouncer, when the
+     * device_entry_udfps_refactor flag is enabled.
+     */
     void dismissKeyguardThenExecute(OnDismissAction action, @Nullable Runnable cancel,
             boolean afterKeyguardGone, @Nullable String customMessage);
 
     /** Starts an activity and dismisses keyguard. */
     void startActivityDismissingKeyguard(Intent intent,
             boolean onlyProvisioned,
-            boolean dismissShade);
+            boolean dismissShade,
+            @Nullable String customMessage);
 
     /** Starts an activity and dismisses keyguard. */
     void startActivityDismissingKeyguard(Intent intent,
diff --git a/packages/SystemUI/res/color/connected_network_primary_color.xml b/packages/SystemUI/res/color/connected_network_primary_color.xml
new file mode 100644
index 0000000..f173c8d
--- /dev/null
+++ b/packages/SystemUI/res/color/connected_network_primary_color.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ 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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <item android:color="?androidprv:attr/materialColorOnPrimaryContainer" />
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_widgets.xml b/packages/SystemUI/res/drawable/ic_widgets.xml
deleted file mode 100644
index b21d047..0000000
--- a/packages/SystemUI/res/drawable/ic_widgets.xml
+++ /dev/null
@@ -1,27 +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.
-  -->
-
-<!-- go/gm2-icons, from gs_widgets_vd_theme_24.xml -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:tint="?attr/colorControlNormal"
-    android:viewportHeight="960"
-    android:viewportWidth="960">
-    <path
-        android:fillColor="@android:color/black"
-        android:pathData="M666,520L440,294L666,68L892,294L666,520ZM120,440L120,120L440,120L440,440L120,440ZM520,840L520,520L840,520L840,840L520,840ZM120,840L120,520L440,520L440,840L120,840ZM200,360L360,360L360,200L200,200L200,360ZM667,408L780,295L667,182L554,295L667,408ZM600,760L760,760L760,600L600,600L600,760ZM200,760L360,760L360,600L200,600L200,760ZM360,360L360,360L360,360L360,360L360,360ZM554,295L554,295L554,295L554,295L554,295ZM360,600L360,600L360,600L360,600L360,600ZM600,600L600,600L600,600L600,600L600,600Z" />
-</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/settingslib_switch_bar_bg_on.xml b/packages/SystemUI/res/drawable/settingslib_switch_bar_bg_on.xml
index 250188b..fab2d8d 100644
--- a/packages/SystemUI/res/drawable/settingslib_switch_bar_bg_on.xml
+++ b/packages/SystemUI/res/drawable/settingslib_switch_bar_bg_on.xml
@@ -16,10 +16,11 @@
   -->
 
 <ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:color="?android:attr/colorControlHighlight">
     <item>
         <shape android:shape="rectangle">
-            <solid android:color="@color/settingslib_state_on_color"/>
+            <solid android:color="?androidprv:attr/materialColorPrimaryContainer"/>
             <corners android:radius="@dimen/settingslib_switch_bar_radius"/>
         </shape>
     </item>
diff --git a/packages/SystemUI/res/drawable/settingslib_thumb_on.xml b/packages/SystemUI/res/drawable/settingslib_thumb_on.xml
index 5566ea3..e316a93 100644
--- a/packages/SystemUI/res/drawable/settingslib_thumb_on.xml
+++ b/packages/SystemUI/res/drawable/settingslib_thumb_on.xml
@@ -15,7 +15,8 @@
   limitations under the License.
   -->
 
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
     <item
         android:top="@dimen/settingslib_switch_thumb_margin"
         android:bottom="@dimen/settingslib_switch_thumb_margin">
@@ -23,7 +24,7 @@
             <size
                 android:height="@dimen/settingslib_switch_thumb_size"
                 android:width="@dimen/settingslib_switch_thumb_size"/>
-            <solid android:color="@color/settingslib_state_on_color"/>
+            <solid android:color="?androidprv:attr/materialColorOnPrimary"/>
         </shape>
     </item>
 </layer-list>
diff --git a/packages/SystemUI/res/drawable/settingslib_track_on_background.xml b/packages/SystemUI/res/drawable/settingslib_track_on_background.xml
index 1d9dacd..e2e6468 100644
--- a/packages/SystemUI/res/drawable/settingslib_track_on_background.xml
+++ b/packages/SystemUI/res/drawable/settingslib_track_on_background.xml
@@ -16,11 +16,12 @@
   -->
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:shape="rectangle"
     android:width="@dimen/settingslib_switch_track_width"
     android:height="@dimen/settingslib_switch_track_height">
     <padding android:left="@dimen/settingslib_switch_thumb_margin"
              android:right="@dimen/settingslib_switch_thumb_margin"/>
-    <solid android:color="@color/settingslib_track_on_color"/>
+    <solid android:color="?androidprv:attr/materialColorPrimary"/>
     <corners android:radius="@dimen/settingslib_switch_track_radius"/>
 </shape>
diff --git a/packages/SystemUI/res/layout/dream_overlay_open_hub_chip.xml b/packages/SystemUI/res/layout/dream_overlay_open_hub_chip.xml
deleted file mode 100644
index be063a9..0000000
--- a/packages/SystemUI/res/layout/dream_overlay_open_hub_chip.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2024 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
--->
-<com.android.systemui.animation.view.LaunchableImageView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_height="@dimen/dream_overlay_bottom_affordance_height"
-    android:layout_width="@dimen/dream_overlay_bottom_affordance_width"
-    android:layout_gravity="bottom|start"
-    android:padding="@dimen/dream_overlay_bottom_affordance_padding"
-    android:scaleType="fitCenter"
-    android:tint="?android:attr/textColorPrimary"
-    android:src="@drawable/ic_widgets"
-    android:contentDescription="@string/accessibility_action_open_communal_hub" />
diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml
index d377e01..21f1cfb 100644
--- a/packages/SystemUI/res/values-night/colors.xml
+++ b/packages/SystemUI/res/values-night/colors.xml
@@ -104,10 +104,6 @@
 
     <color name="people_tile_background">@color/material_dynamic_secondary20</color>
 
-    <!-- Internet Dialog -->
-    <color name="connected_network_primary_color">@color/material_dynamic_primary80</color>
-    <color name="connected_network_secondary_color">@color/material_dynamic_secondary80</color>
-
     <!-- Keyboard shortcut helper dialog -->
     <color name="ksh_key_item_color">@*android:color/system_on_surface_variant_dark</color>
 </resources>
diff --git a/packages/SystemUI/res/values-night/styles.xml b/packages/SystemUI/res/values-night/styles.xml
index 546bf1c..e9dd039f3 100644
--- a/packages/SystemUI/res/values-night/styles.xml
+++ b/packages/SystemUI/res/values-night/styles.xml
@@ -60,18 +60,6 @@
         <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
     </style>
 
-    <style name="TextAppearance.InternetDialog.Active">
-        <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
-        <item name="android:textSize">16sp</item>
-        <item name="android:textColor">@color/material_dynamic_primary80</item>
-        <item name="android:textDirection">locale</item>
-    </style>
-
-    <style name="TextAppearance.InternetDialog.Secondary.Active">
-        <item name="android:textSize">14sp</item>
-        <item name="android:textColor">@color/material_dynamic_secondary80</item>
-    </style>
-
     <style name="ShortcutHelperTheme" parent="@style/ShortcutHelperThemeCommon">
         <item name="android:windowLightNavigationBar">false</item>
     </style>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index b3d3021..0350cd7 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -236,11 +236,8 @@
     <!-- Internet Dialog -->
     <!-- Material next state on color-->
     <color name="settingslib_state_on_color">@color/settingslib_state_on</color>
-    <!-- Material next track on color-->
-    <color name="settingslib_track_on_color">@color/settingslib_track_on</color>
     <!-- Material next track off color-->
     <color name="settingslib_track_off_color">@color/settingslib_track_off</color>
-    <color name="connected_network_primary_color">#191C18</color>
     <color name="connected_network_secondary_color">#41493D</color>
 
     <color name="dream_overlay_camera_mic_off_dot_color">#FCBE03</color>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index ff43c9bc..e92b942 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1193,6 +1193,8 @@
     <string name="popup_on_dismiss_cta_tile_text">Long press to customize widgets</string>
     <!-- Text for the button to configure widgets after long press. [CHAR LIMIT=50] -->
     <string name="button_to_configure_widgets_text">Customize widgets</string>
+    <!-- Text for unlock reason on the bouncer before customizing widgets. [CHAR LIMIT=NONE] -->
+    <string name="unlock_reason_to_customize_widgets">Unlock to customize widgets</string>
     <!-- Description for the App icon of disabled widget. [CHAR LIMIT=NONE] -->
     <string name="icon_description_for_disabled_widget">App icon for disabled widget</string>
     <!-- Description for the App icon of a package that is currently being installed. [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 73b7586..7475eb2 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1315,7 +1315,7 @@
         <item name="android:background">?android:attr/selectableItemBackground</item>
     </style>
 
-    <style name="MainSwitch.Settingslib" parent="@android:style/Theme.DeviceDefault">
+    <style name="MainSwitch.Settingslib" parent="@android:style/Theme.DeviceDefault.DayNight">
         <item name="android:switchMinWidth">@dimen/settingslib_min_switch_width</item>
     </style>
 
@@ -1358,6 +1358,7 @@
 
     <style name="InternetDialog.NetworkTitle.Active">
         <item name="android:textAppearance">@style/TextAppearance.InternetDialog.Active</item>
+        <item name="android:textColor">?androidprv:attr/materialColorOnPrimaryContainer</item>
     </style>
 
     <style name="InternetDialog.NetworkSummary">
@@ -1370,18 +1371,19 @@
     <style name="InternetDialog.NetworkSummary.Active">
         <item name="android:textAppearance">@style/TextAppearance.InternetDialog.Secondary.Active
         </item>
+        <item name="android:textColor">?androidprv:attr/materialColorOnPrimaryContainer</item>
     </style>
 
     <style name="TextAppearance.InternetDialog">
         <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
         <item name="android:textSize">16sp</item>
-        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
         <item name="android:textDirection">locale</item>
     </style>
 
     <style name="TextAppearance.InternetDialog.Secondary">
         <item name="android:textSize">14sp</item>
-        <item name="android:textColor">?android:attr/textColorSecondary</item>
+        <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
     </style>
 
     <style name="TextAppearance.InternetDialog.Active"/>
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index 9d573d3..4a28d8b 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -469,6 +469,7 @@
 
     private void onSpringAnimationsEndAction() {
         if (mShouldShowDockTooltip) {
+            mEduTooltipView.ifPresent(this::removeTooltip);
             mEduTooltipView = Optional.of(new MenuEduTooltipView(mContext, mMenuViewAppearance));
             mEduTooltipView.ifPresent(view -> addTooltipView(view,
                     getContext().getText(R.string.accessibility_floating_button_docking_tooltip),
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
index 94f465d..eaddc42 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
@@ -288,11 +288,13 @@
     private fun startSettingsActivity(intent: Intent, view: View) {
         if (job?.isActive == true) {
             intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
-            activityStarter.postStartActivityDismissingKeyguard(
-                intent,
-                0,
-                dialogTransitionAnimator.createActivityTransitionController(view)
-            )
+            val controller = dialogTransitionAnimator.createActivityTransitionController(view)
+            // The controller will be null when the screen is locked and going to show the
+            // primary bouncer. In this case we dismiss the dialog manually.
+            if (controller == null) {
+                cancelJob()
+            }
+            activityStarter.postStartActivityDismissingKeyguard(intent, 0, controller)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalWidgetCategories.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalWidgetCategories.kt
index 5cd15f2..75f0bad 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalWidgetCategories.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalWidgetCategories.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.communal.data.model
 
 import android.appwidget.AppWidgetProviderInfo
-import com.android.settingslib.flags.Flags.allowAllWidgetsOnLockscreenByDefault
 
 /**
  * The widget categories to display on communal hub (where categories is a bitfield with values that
@@ -31,9 +30,7 @@
         val defaultCategories: Int
             get() {
                 return AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD or
-                    if (allowAllWidgetsOnLockscreenByDefault())
-                        AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
-                    else 0
+                    AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
             }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
index 1c47e50..2940a95 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
@@ -24,7 +24,6 @@
 import com.android.systemui.Flags.communalHub
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.communal.data.model.CommunalEnabledState
-import com.android.systemui.communal.data.model.CommunalWidgetCategories
 import com.android.systemui.communal.data.model.DisabledReason
 import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_DEVICE_POLICY
 import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_FLAG
@@ -52,12 +51,6 @@
     /** A [CommunalEnabledState] for the specified user. */
     fun getEnabledState(user: UserInfo): Flow<CommunalEnabledState>
 
-    /**
-     * A flow that reports the widget categories to show on the hub as selected by the user in
-     * Settings.
-     */
-    fun getWidgetCategories(user: UserInfo): Flow<CommunalWidgetCategories>
-
     /** Keyguard widgets enabled state by Device Policy Manager for the specified user. */
     fun getAllowedByDevicePolicy(user: UserInfo): Flow<Boolean>
 
@@ -104,22 +97,6 @@
             .flowOn(bgDispatcher)
     }
 
-    override fun getWidgetCategories(user: UserInfo): Flow<CommunalWidgetCategories> =
-        secureSettings
-            .observerFlow(userId = user.id, names = arrayOf(GLANCEABLE_HUB_CONTENT_SETTING))
-            // Force an update
-            .onStart { emit(Unit) }
-            .map {
-                CommunalWidgetCategories(
-                    secureSettings.getIntForUser(
-                        GLANCEABLE_HUB_CONTENT_SETTING,
-                        CommunalWidgetCategories.defaultCategories,
-                        user.id
-                    )
-                )
-            }
-            .flowOn(bgDispatcher)
-
     override fun getAllowedByDevicePolicy(user: UserInfo): Flow<Boolean> =
         broadcastDispatcher
             .broadcastFlow(
@@ -159,7 +136,6 @@
             }
 
     companion object {
-        const val GLANCEABLE_HUB_CONTENT_SETTING = "glanceable_hub_content_setting"
         const val GLANCEABLE_HUB_BACKGROUND_SETTING = "glanceable_hub_background"
         private const val ENABLED_SETTING_DEFAULT = 1
     }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 9f3ade9..f5255ac 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -392,26 +392,17 @@
                     allowedForWorkProfile ->
                     filterWidgetsAllowedByDevicePolicy(widgets, allowedForWorkProfile)
                 },
-            communalSettingsInteractor.communalWidgetCategories,
             updateOnWorkProfileBroadcastReceived,
-        ) { widgets, allowedCategories, _ ->
+        ) { widgets, _ ->
             widgets.map { widget ->
                 when (widget) {
                     is CommunalWidgetContentModel.Available -> {
-                        if (widget.providerInfo.widgetCategory and allowedCategories != 0) {
-                            // At least one category this widget specified is allowed, so show it
-                            WidgetContent.Widget(
-                                appWidgetId = widget.appWidgetId,
-                                providerInfo = widget.providerInfo,
-                                appWidgetHost = appWidgetHost,
-                                inQuietMode = isQuietModeEnabled(widget.providerInfo.profile)
-                            )
-                        } else {
-                            WidgetContent.DisabledWidget(
-                                appWidgetId = widget.appWidgetId,
-                                providerInfo = widget.providerInfo,
-                            )
-                        }
+                        WidgetContent.Widget(
+                            appWidgetId = widget.appWidgetId,
+                            providerInfo = widget.providerInfo,
+                            appWidgetHost = appWidgetHost,
+                            inQuietMode = isQuietModeEnabled(widget.providerInfo.profile)
+                        )
                     }
                     is CommunalWidgetContentModel.Pending -> {
                         WidgetContent.PendingWidget(
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt
index f043d58..47b75c4 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt
@@ -19,7 +19,6 @@
 import android.content.pm.UserInfo
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.communal.data.model.CommunalEnabledState
-import com.android.systemui.communal.data.model.CommunalWidgetCategories
 import com.android.systemui.communal.data.repository.CommunalSettingsRepository
 import com.android.systemui.communal.shared.model.CommunalBackgroundType
 import com.android.systemui.dagger.SysUISingleton
@@ -70,18 +69,6 @@
             // Start this eagerly since the value is accessed synchronously in many places.
             .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false)
 
-    /** What widget categories to show on the hub. */
-    val communalWidgetCategories: StateFlow<Int> =
-        userInteractor.selectedUserInfo
-            .flatMapLatest { user -> repository.getWidgetCategories(user) }
-            .map { categories -> categories.categories }
-            .stateIn(
-                scope = bgScope,
-                // Start this eagerly since the value can be accessed synchronously.
-                started = SharingStarted.Eagerly,
-                initialValue = CommunalWidgetCategories.defaultCategories
-            )
-
     /** The type of background to use for the hub. Used to experiment with different backgrounds */
     val communalBackground: Flow<CommunalBackgroundType> =
         userInteractor.selectedUserInfo
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 9185384..fab2435 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -25,6 +25,7 @@
 import androidx.activity.result.ActivityResultLauncher
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.Flags.enableWidgetPickerSizeFilter
+import com.android.systemui.communal.data.model.CommunalWidgetCategories
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.CommunalPrefsInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
@@ -183,7 +184,7 @@
             }
             putExtra(
                 AppWidgetManager.EXTRA_CATEGORY_FILTER,
-                communalSettingsInteractor.communalWidgetCategories.value
+                CommunalWidgetCategories.defaultCategories
             )
             putExtra(EXTRA_UI_SURFACE_KEY, EXTRA_UI_SURFACE_VALUE)
             putParcelableArrayListExtra(EXTRA_ADDED_APP_WIDGETS_KEY, excludeList)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt
index 76be005..af87f09 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.communal.widgets.EditWidgetsActivity.Companion.EXTRA_PRESELECTED_KEY
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.res.R
 import javax.inject.Inject
 
 interface EditWidgetsActivityStarter {
@@ -48,6 +49,7 @@
                 },
             /* onlyProvisioned = */ true,
             /* dismissShade = */ true,
+            applicationContext.resources.getString(R.string.unlock_reason_to_customize_widgets),
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
index cbc6c97..72f9180 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
@@ -34,10 +34,11 @@
     private val activityStarter: ActivityStarter,
 ) : RemoteViews.InteractionHandler {
 
-    private val delegate = InteractionHandlerDelegate(
-        findViewToAnimate = { view -> view is CommunalAppWidgetHostView },
-        intentStarter = this::startIntent,
-    )
+    private val delegate =
+        InteractionHandlerDelegate(
+            findViewToAnimate = { view -> view is CommunalAppWidgetHostView },
+            intentStarter = this::startIntent,
+        )
 
     override fun onInteraction(
         view: View,
@@ -45,7 +46,6 @@
         response: RemoteViews.RemoteResponse
     ): Boolean = delegate.onInteraction(view, pendingIntent, response)
 
-
     private fun startIntent(
         pendingIntent: PendingIntent,
         fillInIntent: Intent,
@@ -59,6 +59,8 @@
             controller,
             fillInIntent,
             extraOptions.toBundle(),
+            // TODO(b/325110448): UX to provide copy
+            /* customMessage = */ null,
         )
         return true
     }
diff --git a/packages/SystemUI/src/com/android/systemui/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/complication/DreamHomeControlsComplication.java
index 92108e9..afa2375 100644
--- a/packages/SystemUI/src/com/android/systemui/complication/DreamHomeControlsComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/complication/DreamHomeControlsComplication.java
@@ -18,7 +18,6 @@
 
 import static com.android.systemui.complication.dagger.DreamHomeControlsComplicationComponent.DreamHomeControlsModule.DREAM_HOME_CONTROLS_CHIP_VIEW;
 import static com.android.systemui.complication.dagger.RegisteredComplicationsModule.DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS;
-import static com.android.systemui.complication.dagger.RegisteredComplicationsModule.OPEN_HUB_CHIP_REPLACE_HOME_CONTROLS;
 import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE;
 import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE_AFTER_UNLOCK;
 import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.UNAVAILABLE;
@@ -90,7 +89,6 @@
         private final DreamHomeControlsComplication mComplication;
         private final DreamOverlayStateController mDreamOverlayStateController;
         private final ControlsComponent mControlsComponent;
-        private final boolean mReplacedByOpenHub;
 
         private boolean mOverlayActive = false;
 
@@ -118,13 +116,11 @@
         public Registrant(DreamHomeControlsComplication complication,
                 DreamOverlayStateController dreamOverlayStateController,
                 ControlsComponent controlsComponent,
-                @SystemUser Monitor monitor,
-                @Named(OPEN_HUB_CHIP_REPLACE_HOME_CONTROLS) boolean replacedByOpenHub) {
+                @SystemUser Monitor monitor) {
             super(monitor);
             mComplication = complication;
             mControlsComponent = controlsComponent;
             mDreamOverlayStateController = dreamOverlayStateController;
-            mReplacedByOpenHub = replacedByOpenHub;
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/complication/OpenHubComplication.java b/packages/SystemUI/src/com/android/systemui/complication/OpenHubComplication.java
deleted file mode 100644
index 05df2bb..0000000
--- a/packages/SystemUI/src/com/android/systemui/complication/OpenHubComplication.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.complication;
-
-import static com.android.systemui.complication.dagger.OpenHubComplicationComponent.OpenHubModule.OPEN_HUB_CHIP_VIEW;
-import static com.android.systemui.complication.dagger.RegisteredComplicationsModule.OPEN_HUB_CHIP_LAYOUT_PARAMS;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.util.Log;
-import android.view.View;
-import android.widget.ImageView;
-
-import com.android.settingslib.Utils;
-import com.android.systemui.CoreStartable;
-import com.android.systemui.communal.domain.interactor.CommunalInteractor;
-import com.android.systemui.communal.shared.model.CommunalScenes;
-import com.android.systemui.complication.dagger.OpenHubComplicationComponent;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.dagger.qualifiers.SystemUser;
-import com.android.systemui.dreams.DreamOverlayStateController;
-import com.android.systemui.shared.condition.Monitor;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.util.ViewController;
-import com.android.systemui.util.condition.ConditionalCoreStartable;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-/**
- * A dream complication that shows a chip to open the glanceable hub.
- */
-// TODO(b/339667383): delete or properly implement this once a product decision is made
-public class OpenHubComplication implements Complication {
-    private final Resources mResources;
-    private final OpenHubComplicationComponent.Factory mComponentFactory;
-
-    @Inject
-    public OpenHubComplication(
-            @Main Resources resources,
-            OpenHubComplicationComponent.Factory componentFactory) {
-        mResources = resources;
-        mComponentFactory = componentFactory;
-    }
-
-    @Override
-    public ViewHolder createView(ComplicationViewModel model) {
-        return mComponentFactory.create(mResources).getViewHolder();
-    }
-
-    @Override
-    public int getRequiredTypeAvailability() {
-        // TODO(b/339667383): create a new complication type if we decide to productionize this
-        return COMPLICATION_TYPE_NONE;
-    }
-
-    /**
-     * {@link CoreStartable} for registering the complication with SystemUI on startup.
-     */
-    public static class Registrant extends ConditionalCoreStartable {
-        private final OpenHubComplication mComplication;
-        private final DreamOverlayStateController mDreamOverlayStateController;
-
-        private boolean mOverlayActive = false;
-
-        private final DreamOverlayStateController.Callback mOverlayStateCallback =
-                new DreamOverlayStateController.Callback() {
-                    @Override
-                    public void onStateChanged() {
-                        if (mOverlayActive == mDreamOverlayStateController.isOverlayActive()) {
-                            return;
-                        }
-
-                        mOverlayActive = !mOverlayActive;
-
-                        if (mOverlayActive) {
-                            updateOpenHubComplication();
-                        }
-                    }
-                };
-
-        @Inject
-        public Registrant(OpenHubComplication complication,
-                DreamOverlayStateController dreamOverlayStateController,
-                @SystemUser Monitor monitor) {
-            super(monitor);
-            mComplication = complication;
-            mDreamOverlayStateController = dreamOverlayStateController;
-        }
-
-        @Override
-        public void onStart() {
-            mDreamOverlayStateController.addCallback(mOverlayStateCallback);
-        }
-
-        private void updateOpenHubComplication() {
-            // TODO(b/339667383): don't show the complication if glanceable hub is disabled
-//            if (Flags.glanceableHubShortcutButton()) {
-//                mDreamOverlayStateController.addComplication(mComplication);
-//            } else {
-//                mDreamOverlayStateController.removeComplication(mComplication);
-//            }
-        }
-    }
-
-    /**
-     * Contains values/logic associated with the dream complication view.
-     */
-    public static class OpenHubChipViewHolder implements ViewHolder {
-        private final ImageView mView;
-        private final ComplicationLayoutParams mLayoutParams;
-        private final OpenHubChipViewController mViewController;
-
-        @Inject
-        OpenHubChipViewHolder(
-                OpenHubChipViewController dreamOpenHubChipViewController,
-                @Named(OPEN_HUB_CHIP_VIEW) ImageView view,
-                @Named(OPEN_HUB_CHIP_LAYOUT_PARAMS) ComplicationLayoutParams layoutParams
-        ) {
-            mView = view;
-            mLayoutParams = layoutParams;
-            mViewController = dreamOpenHubChipViewController;
-            mViewController.init();
-        }
-
-        @Override
-        public ImageView getView() {
-            return mView;
-        }
-
-        @Override
-        public ComplicationLayoutParams getLayoutParams() {
-            return mLayoutParams;
-        }
-    }
-
-    /**
-     * Controls behavior of the dream complication.
-     */
-    static class OpenHubChipViewController extends ViewController<ImageView> {
-        private static final String TAG = "OpenHubCtrl";
-        private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-
-        private final Context mContext;
-        private final ConfigurationController mConfigurationController;
-
-        private final ConfigurationController.ConfigurationListener mConfigurationListener =
-                new ConfigurationController.ConfigurationListener() {
-                    @Override
-                    public void onUiModeChanged() {
-                        reloadResources();
-                    }
-                };
-        private final CommunalInteractor mCommunalInteractor;
-
-        @Inject
-        OpenHubChipViewController(
-                @Named(OPEN_HUB_CHIP_VIEW) ImageView view,
-                Context context,
-                ConfigurationController configurationController,
-                CommunalInteractor communalInteractor) {
-            super(view);
-
-            mContext = context;
-            mConfigurationController = configurationController;
-            mCommunalInteractor = communalInteractor;
-        }
-
-        @Override
-        protected void onViewAttached() {
-            reloadResources();
-            mView.setOnClickListener(this::onClickOpenHub);
-            mConfigurationController.addCallback(mConfigurationListener);
-        }
-
-        @Override
-        protected void onViewDetached() {
-            mConfigurationController.removeCallback(mConfigurationListener);
-        }
-
-        private void reloadResources() {
-            mView.setImageTintList(Utils.getColorAttr(mContext, android.R.attr.textColorPrimary));
-            final Drawable background = mView.getBackground();
-            if (background != null) {
-                background.setTintList(
-                        Utils.getColorAttr(mContext, com.android.internal.R.attr.colorSurface));
-            }
-        }
-
-        private void onClickOpenHub(View v) {
-            if (DEBUG) Log.d(TAG, "open hub complication tapped");
-
-            mCommunalInteractor.changeScene(CommunalScenes.Communal, null);
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/complication/dagger/OpenHubComplicationComponent.java b/packages/SystemUI/src/com/android/systemui/complication/dagger/OpenHubComplicationComponent.java
deleted file mode 100644
index 501601e..0000000
--- a/packages/SystemUI/src/com/android/systemui/complication/dagger/OpenHubComplicationComponent.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.complication.dagger;
-
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.view.LayoutInflater;
-import android.widget.ImageView;
-
-import com.android.systemui.complication.OpenHubComplication;
-import com.android.systemui.res.R;
-import com.android.systemui.shared.shadow.DoubleShadowIconDrawable;
-import com.android.systemui.shared.shadow.DoubleShadowTextHelper;
-
-import dagger.BindsInstance;
-import dagger.Module;
-import dagger.Provides;
-import dagger.Subcomponent;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.Retention;
-
-import javax.inject.Named;
-import javax.inject.Scope;
-
-/**
- * Responsible for generating dependencies for the {@link OpenHubComplication}.
- */
-@Subcomponent(modules = OpenHubComplicationComponent.OpenHubModule.class)
-@OpenHubComplicationComponent.OpenHubComplicationScope
-public interface OpenHubComplicationComponent {
-    /**
-     * Creates a view holder for the open hub complication.
-     */
-    OpenHubComplication.OpenHubChipViewHolder getViewHolder();
-
-    /**
-     * Scope of the open hub complication.
-     */
-    @Documented
-    @Retention(RUNTIME)
-    @Scope
-    @interface OpenHubComplicationScope {
-    }
-
-    /**
-     * Factory that generates a {@link OpenHubComplicationComponent}.
-     */
-    @Subcomponent.Factory
-    interface Factory {
-        /**
-         * Creates an instance of {@link OpenHubComplicationComponent}.
-         */
-        OpenHubComplicationComponent create(@BindsInstance Resources resources);
-    }
-
-    /**
-     * Scoped injected values for the {@link OpenHubComplicationComponent}.
-     */
-    @Module
-    interface OpenHubModule {
-        String OPEN_HUB_CHIP_VIEW = "open_hub_chip_view";
-        String OPEN_HUB_BACKGROUND_DRAWABLE = "open_hub_background_drawable";
-
-        /**
-         * Provides the dream open hub chip view.
-         */
-        @Provides
-        @OpenHubComplicationScope
-        @Named(OPEN_HUB_CHIP_VIEW)
-        static ImageView provideOpenHubChipView(
-                LayoutInflater layoutInflater,
-                @Named(OPEN_HUB_BACKGROUND_DRAWABLE) Drawable backgroundDrawable) {
-            final ImageView chip =
-                    (ImageView) layoutInflater.inflate(R.layout.dream_overlay_open_hub_chip,
-                            null, false);
-            chip.setBackground(backgroundDrawable);
-
-            return chip;
-        }
-
-        /**
-         * Provides the background drawable for the open hub chip.
-         */
-        @Provides
-        @OpenHubComplicationScope
-        @Named(OPEN_HUB_BACKGROUND_DRAWABLE)
-        static Drawable providesOpenHubBackground(Context context, Resources resources) {
-            return new DoubleShadowIconDrawable(createShadowInfo(
-                    resources,
-                    R.dimen.dream_overlay_bottom_affordance_key_text_shadow_radius,
-                    R.dimen.dream_overlay_bottom_affordance_key_text_shadow_dx,
-                    R.dimen.dream_overlay_bottom_affordance_key_text_shadow_dy,
-                    R.dimen.dream_overlay_bottom_affordance_key_shadow_alpha
-            ),
-                    createShadowInfo(
-                            resources,
-                            R.dimen.dream_overlay_bottom_affordance_ambient_text_shadow_radius,
-                            R.dimen.dream_overlay_bottom_affordance_ambient_text_shadow_dx,
-                            R.dimen.dream_overlay_bottom_affordance_ambient_text_shadow_dy,
-                            R.dimen.dream_overlay_bottom_affordance_ambient_shadow_alpha
-                    ),
-                    resources.getDrawable(R.drawable.dream_overlay_bottom_affordance_bg),
-                    resources.getDimensionPixelOffset(
-                            R.dimen.dream_overlay_bottom_affordance_width),
-                    resources.getDimensionPixelSize(R.dimen.dream_overlay_bottom_affordance_inset)
-            );
-        }
-
-        private static DoubleShadowTextHelper.ShadowInfo createShadowInfo(Resources resources,
-                int blurId, int offsetXId, int offsetYId, int alphaId) {
-
-            return new DoubleShadowTextHelper.ShadowInfo(
-                    resources.getDimension(blurId),
-                    resources.getDimension(offsetXId),
-                    resources.getDimension(offsetYId),
-                    resources.getFloat(alphaId)
-            );
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/complication/dagger/RegisteredComplicationsModule.java b/packages/SystemUI/src/com/android/systemui/complication/dagger/RegisteredComplicationsModule.java
index edb5ff7..6f1b098 100644
--- a/packages/SystemUI/src/com/android/systemui/complication/dagger/RegisteredComplicationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/complication/dagger/RegisteredComplicationsModule.java
@@ -25,7 +25,6 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.res.R;
-import com.android.systemui.util.settings.SystemSettings;
 
 import dagger.Module;
 import dagger.Provides;
@@ -40,7 +39,6 @@
         subcomponents = {
                 DreamClockTimeComplicationComponent.class,
                 DreamHomeControlsComplicationComponent.class,
-                OpenHubComplicationComponent.class,
                 DreamMediaEntryComplicationComponent.class
         })
 public interface RegisteredComplicationsModule {
@@ -48,8 +46,6 @@
     String DREAM_SMARTSPACE_LAYOUT_PARAMS = "smartspace_layout_params";
     String DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS = "home_controls_chip_layout_params";
     String DREAM_MEDIA_ENTRY_LAYOUT_PARAMS = "media_entry_layout_params";
-    String OPEN_HUB_CHIP_LAYOUT_PARAMS = "open_hub_chip_layout_params";
-    String OPEN_HUB_CHIP_REPLACE_HOME_CONTROLS = "open_hub_chip_replace_home_controls";
 
     int DREAM_CLOCK_TIME_COMPLICATION_WEIGHT = 1;
     int DREAM_CLOCK_TIME_COMPLICATION_WEIGHT_NO_SMARTSPACE = 2;
@@ -113,26 +109,6 @@
     }
 
     /**
-     * Provides layout parameters for the open hub complication.
-     */
-    @Provides
-    @Named(OPEN_HUB_CHIP_LAYOUT_PARAMS)
-    static ComplicationLayoutParams provideOpenHubLayoutParams(
-            @Named(OPEN_HUB_CHIP_REPLACE_HOME_CONTROLS) boolean replaceHomeControls) {
-        int position = ComplicationLayoutParams.POSITION_BOTTOM | (replaceHomeControls
-                ? ComplicationLayoutParams.POSITION_START
-                : ComplicationLayoutParams.POSITION_END);
-        int direction = replaceHomeControls ? ComplicationLayoutParams.DIRECTION_END
-                : ComplicationLayoutParams.DIRECTION_START;
-        return new ComplicationLayoutParams(
-                ViewGroup.LayoutParams.WRAP_CONTENT,
-                ViewGroup.LayoutParams.WRAP_CONTENT,
-                position,
-                direction,
-                DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT);
-    }
-
-    /**
      * Provides layout parameters for the smartspace complication.
      */
     @Provides
@@ -148,14 +124,4 @@
                 res.getDimensionPixelSize(R.dimen.dream_overlay_complication_smartspace_padding),
                 res.getDimensionPixelSize(R.dimen.dream_overlay_complication_smartspace_max_width));
     }
-
-    /**
-     * If true, the home controls chip should not be shown and the open hub chip should be shown in
-     * its place.
-     */
-    @Provides
-    @Named(OPEN_HUB_CHIP_REPLACE_HOME_CONTROLS)
-    static boolean providesOpenHubChipReplaceHomeControls(SystemSettings systemSettings) {
-        return systemSettings.getBool(OPEN_HUB_CHIP_REPLACE_HOME_CONTROLS, false);
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index 1e4fb4f..c1de381 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -51,10 +51,14 @@
 import android.database.ContentObserver;
 import android.graphics.Color;
 import android.graphics.drawable.Drawable;
+import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.Flags;
 import android.media.AudioManager;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.CancellationSignal;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
@@ -857,6 +861,29 @@
             if (ActivityManager.isUserAMonkey()) {
                 return;
             }
+            if (Flags.mandatoryBiometrics()
+                    && requestBiometricAuthenticationForMandatoryBiometrics()) {
+                launchBiometricPromptForMandatoryBiometrics(
+                        new BiometricPrompt.AuthenticationCallback() {
+                            @Override
+                            public void onAuthenticationError(int errorCode,
+                                    CharSequence errString) {
+                                super.onAuthenticationError(errorCode, errString);
+                            }
+
+                            @Override
+                            public void onAuthenticationSucceeded(
+                                    BiometricPrompt.AuthenticationResult result) {
+                                super.onAuthenticationSucceeded(result);
+                                shutDown();
+                            }
+                        });
+            } else {
+                shutDown();
+            }
+        }
+
+        private void shutDown() {
             mUiEventLogger.log(GlobalActionsEvent.GA_SHUTDOWN_PRESS);
             // shutdown by making sure radio and power are handled accordingly.
             mWindowManagerFuncs.shutdown();
@@ -2261,6 +2288,35 @@
     }
 
     @VisibleForTesting
+    void launchBiometricPromptForMandatoryBiometrics(
+            BiometricPrompt.AuthenticationCallback authenticationCallback) {
+        final CancellationSignal cancellationSignal = new CancellationSignal();
+        final BiometricPrompt biometricPrompt = new BiometricPrompt.Builder(mContext)
+                .setAllowedAuthenticators(BiometricManager.Authenticators.MANDATORY_BIOMETRICS)
+                .setUseDefaultTitle()
+                .setDescription(mContext.getString(
+                        R.string.identity_check_biometric_prompt_description))
+                .setNegativeButton(mContext.getString(R.string.cancel), mContext.getMainExecutor(),
+                        (dialog, which) -> cancellationSignal.cancel())
+                .setAllowBackgroundAuthentication(true)
+                .build();
+        biometricPrompt.authenticate(cancellationSignal, mContext.getMainExecutor(),
+                authenticationCallback);
+    }
+
+    private boolean requestBiometricAuthenticationForMandatoryBiometrics() {
+        final BiometricManager biometricManager =
+                (BiometricManager) mContext.getSystemService(Context.BIOMETRIC_SERVICE);
+        if (biometricManager == null) {
+            Log.e(TAG, "Biometric Manager is null.");
+            return false;
+        }
+        final int status = biometricManager.canAuthenticate(
+                BiometricManager.Authenticators.MANDATORY_BIOMETRICS);
+        return status == BiometricManager.BIOMETRIC_SUCCESS;
+    }
+
+    @VisibleForTesting
     static class ActionsDialogLite extends SystemUIDialog implements DialogInterface,
             ColorExtractor.OnColorsChangedListener {
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
index 04bde26..c00bd6f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
@@ -16,11 +16,23 @@
 
 package com.android.systemui.keyboard.shortcut.data.repository
 
+import android.view.KeyEvent
+import android.view.KeyboardShortcutGroup
+import android.view.KeyboardShortcutInfo
+import android.view.WindowManager
+import android.view.WindowManager.KeyboardShortcutsReceiver
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyboard.shortcut.data.source.MultitaskingShortcutsSource
 import com.android.systemui.keyboard.shortcut.data.source.SystemShortcutsSource
+import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
+import com.android.systemui.keyboard.shortcut.shared.model.shortcutCategory
 import javax.inject.Inject
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.suspendCancellableCoroutine
 
 @SysUISingleton
 class ShortcutHelperCategoriesRepository
@@ -28,11 +40,77 @@
 constructor(
     private val systemShortcutsSource: SystemShortcutsSource,
     private val multitaskingShortcutsSource: MultitaskingShortcutsSource,
+    private val windowManager: WindowManager,
+    shortcutHelperStateRepository: ShortcutHelperStateRepository
 ) {
 
-    fun systemShortcutsCategory(): ShortcutCategory =
-        systemShortcutsSource.systemShortcutsCategory()
+    val systemShortcutsCategory =
+        shortcutHelperStateRepository.state.map {
+            if (it is Active) systemShortcutsSource.systemShortcutsCategory() else null
+        }
 
-    fun multitaskingShortcutsCategory(): ShortcutCategory =
-        multitaskingShortcutsSource.multitaskingShortcutCategory()
+    val multitaskingShortcutsCategory =
+        shortcutHelperStateRepository.state.map {
+            if (it is Active) multitaskingShortcutsSource.multitaskingShortcutCategory() else null
+        }
+
+    val imeShortcutsCategory =
+        shortcutHelperStateRepository.state.map {
+            if (it is Active) retrieveImeShortcuts(it.deviceId) else null
+        }
+
+    private suspend fun retrieveImeShortcuts(deviceId: Int): ShortcutCategory {
+        return suspendCancellableCoroutine { continuation ->
+            val shortcutsReceiver = KeyboardShortcutsReceiver { shortcutGroups ->
+                continuation.resumeWith(Result.success(toShortcutCategory(shortcutGroups)))
+            }
+            windowManager.requestImeKeyboardShortcuts(shortcutsReceiver, deviceId)
+        }
+    }
+
+    private fun toShortcutCategory(shortcutGroups: List<KeyboardShortcutGroup>) =
+        shortcutCategory(ShortcutCategoryType.IME) {
+            shortcutGroups.map { shortcutGroup ->
+                subCategory(shortcutGroup.label.toString(), toShortcuts(shortcutGroup.items))
+            }
+        }
+
+    private fun toShortcuts(infoList: List<KeyboardShortcutInfo>) =
+        infoList.mapNotNull { toShortcut(it) }
+
+    private fun toShortcut(shortcutInfo: KeyboardShortcutInfo): Shortcut? {
+        val shortcutCommand = toShortcutCommand(shortcutInfo)
+        return if (shortcutCommand == null) null
+        else Shortcut(label = shortcutInfo.label!!.toString(), commands = listOf(shortcutCommand))
+    }
+
+    private fun toShortcutCommand(info: KeyboardShortcutInfo): ShortcutCommand? {
+        val keyCodes = mutableListOf<Int>()
+        var remainingModifiers = info.modifiers
+        SUPPORTED_MODIFIERS.forEach { supportedModifier ->
+            if ((supportedModifier and remainingModifiers) != 0) {
+                keyCodes += supportedModifier
+                // "Remove" the modifier from the remaining modifiers
+                remainingModifiers = remainingModifiers and supportedModifier.inv()
+            }
+        }
+        if (remainingModifiers != 0) {
+            // There is a remaining modifier we don't support
+            return null
+        }
+        keyCodes += info.keycode
+        return ShortcutCommand(keyCodes)
+    }
+
+    companion object {
+        private val SUPPORTED_MODIFIERS =
+            listOf(
+                KeyEvent.META_META_ON,
+                KeyEvent.META_CTRL_ON,
+                KeyEvent.META_ALT_ON,
+                KeyEvent.META_SHIFT_ON,
+                KeyEvent.META_SYM_ON,
+                KeyEvent.META_FUNCTION_ON
+            )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
index 883407c..57d4b4a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
@@ -18,30 +18,56 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperCategoriesRepository
-import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository
+import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.map
 
 @SysUISingleton
 class ShortcutHelperCategoriesInteractor
 @Inject
 constructor(
-    stateRepository: ShortcutHelperStateRepository,
     categoriesRepository: ShortcutHelperCategoriesRepository,
 ) {
 
+    private val systemsShortcutCategory = categoriesRepository.systemShortcutsCategory
+    private val multitaskingShortcutsCategory = categoriesRepository.multitaskingShortcutsCategory
+    private val imeShortcutsCategory =
+        categoriesRepository.imeShortcutsCategory.map { groupSubCategoriesInCategory(it) }
+
     val shortcutCategories: Flow<List<ShortcutCategory>> =
-        stateRepository.state.map { state ->
-            when (state) {
-                is ShortcutHelperState.Active ->
-                    listOf(
-                        categoriesRepository.systemShortcutsCategory(),
-                        categoriesRepository.multitaskingShortcutsCategory()
-                    )
-                is ShortcutHelperState.Inactive -> emptyList()
-            }
+        combine(systemsShortcutCategory, multitaskingShortcutsCategory, imeShortcutsCategory) {
+            shortcutCategories ->
+            shortcutCategories.filterNotNull()
         }
+
+    private fun groupSubCategoriesInCategory(
+        shortcutCategory: ShortcutCategory?
+    ): ShortcutCategory? {
+        if (shortcutCategory == null) {
+            return null
+        }
+        val subCategoriesWithGroupedShortcuts =
+            shortcutCategory.subCategories.map {
+                ShortcutSubCategory(
+                    label = it.label,
+                    shortcuts = groupShortcutsInSubcategory(it.shortcuts)
+                )
+            }
+        return ShortcutCategory(
+            type = shortcutCategory.type,
+            subCategories = subCategoriesWithGroupedShortcuts
+        )
+    }
+
+    private fun groupShortcutsInSubcategory(shortcuts: List<Shortcut>) =
+        shortcuts
+            .groupBy { it.label }
+            .entries
+            .map { (commonLabel, groupedShortcuts) ->
+                Shortcut(label = commonLabel, commands = groupedShortcuts.flatMap { it.commands })
+            }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperStateInteractor.kt
index 3d707f7..299628e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperStateInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperStateInteractor.kt
@@ -26,6 +26,7 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -38,7 +39,7 @@
     private val repository: ShortcutHelperStateRepository
 ) {
 
-    val state: Flow<ShortcutHelperState> = repository.state
+    val state: Flow<ShortcutHelperState> = repository.state.asStateFlow()
 
     fun onViewClosed() {
         repository.hide()
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
index c5e8d2c..3ac7fa8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
@@ -19,6 +19,7 @@
 enum class ShortcutCategoryType {
     SYSTEM,
     MULTI_TASKING,
+    IME
 }
 
 data class ShortcutCategory(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index a7e2633..5f6fe1c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -105,6 +105,13 @@
 const val UNLOCK_ANIMATION_DURATION_MS = 167L
 
 /**
+ * If there are two different wallpapers on home and lock screen, duration and delay of the lock
+ * wallpaper fade out.
+ */
+const val LOCK_WALLPAPER_FADE_OUT_DURATION = 140L
+const val LOCK_WALLPAPER_FADE_OUT_START_DELAY = 0L
+
+/**
  * How long the in-window launcher icon animation takes. This is used if the launcher is underneath
  * the lock screen and supports in-window animations.
  *
@@ -115,23 +122,24 @@
 
 /**
  * How long to wait for the shade to get out of the way before starting the canned unlock animation.
+ * If there are two different wallpapers on home and lock screen, this is also the duration and
+ * delay of the home wallpaper fade in.
  */
 const val LEGACY_CANNED_UNLOCK_START_DELAY = 100L
-const val CANNED_UNLOCK_START_DELAY = 67L
+const val CANNED_UNLOCK_START_DELAY = 25L
 
 /**
  * Duration for the alpha animation on the surface behind. This plays to fade in the surface during
  * a swipe to unlock (and to fade it back out if the swipe is cancelled).
  */
-const val LEGACY_SURFACE_BEHIND_SWIPE_FADE_DURATION_MS = 175L
-const val SURFACE_BEHIND_FADE_OUT_DURATION_MS = 83L
+const val SURFACE_BEHIND_SWIPE_FADE_DURATION_MS = 175L
 
 /**
  * Start delay for the surface behind animation, used so that the lockscreen can get out of the way
  * before the surface begins appearing.
  */
 const val LEGACY_UNLOCK_ANIMATION_SURFACE_BEHIND_START_DELAY_MS = 75L
-const val SURFACE_BEHIND_FADE_OUT_START_DELAY_MS = 0L
+const val UNLOCK_ANIMATION_SURFACE_BEHIND_START_DELAY_MS = 67L
 
 /**
  * Initiates, controls, and ends the keyguard unlock animation.
@@ -268,7 +276,8 @@
     @VisibleForTesting
     var surfaceTransactionApplier: SyncRtSurfaceTransactionApplier? = null
     private var surfaceBehindRemoteAnimationTargets: Array<RemoteAnimationTarget>? = null
-    private var wallpaperTargets: Array<RemoteAnimationTarget>? = null
+    private var openingWallpaperTargets: Array<RemoteAnimationTarget>? = null
+    private var closingWallpaperTargets: Array<RemoteAnimationTarget>? = null
     private var surfaceBehindRemoteAnimationStartTime: Long = 0
 
     /**
@@ -286,6 +295,8 @@
 
     var wallpaperCannedUnlockAnimator = ValueAnimator.ofFloat(0f, 1f)
 
+    var wallpaperFadeOutUnlockAnimator = ValueAnimator.ofFloat(1f, 0f)
+
     /**
      * Matrix applied to [surfaceBehindRemoteAnimationTarget], which is the surface of the
      * app/launcher behind the keyguard.
@@ -335,7 +346,7 @@
 
     init {
         with(surfaceBehindAlphaAnimator) {
-            duration = surfaceBehindFadeOutDurationMs()
+            duration = SURFACE_BEHIND_SWIPE_FADE_DURATION_MS
             interpolator = Interpolators.LINEAR
             addUpdateListener { valueAnimator: ValueAnimator ->
                 surfaceBehindAlpha = valueAnimator.animatedValue as Float
@@ -351,7 +362,8 @@
                     if (surfaceBehindAlpha == 0f) {
                         Log.d(TAG, "surfaceBehindAlphaAnimator#onAnimationEnd")
                         surfaceBehindRemoteAnimationTargets = null
-                        wallpaperTargets = null
+                        openingWallpaperTargets = null
+                        closingWallpaperTargets = null
                         keyguardViewMediator.get().finishSurfaceBehindRemoteAnimation(
                             false /* cancelled */)
                     } else {
@@ -367,8 +379,10 @@
                     else LAUNCHER_ICONS_ANIMATION_DURATION_MS
             interpolator = if (fasterUnlockTransition()) Interpolators.LINEAR
                     else Interpolators.ALPHA_OUT
+            if (fasterUnlockTransition()) startDelay = CANNED_UNLOCK_START_DELAY
             addUpdateListener { valueAnimator: ValueAnimator ->
-                setWallpaperAppearAmount(valueAnimator.animatedValue as Float)
+                setWallpaperAppearAmount(
+                        valueAnimator.animatedValue as Float, openingWallpaperTargets)
             }
             addListener(object : AnimatorListenerAdapter() {
                 override fun onAnimationEnd(animation: Animator) {
@@ -379,6 +393,18 @@
             })
         }
 
+        if (fasterUnlockTransition()) {
+            with(wallpaperFadeOutUnlockAnimator) {
+                duration = LOCK_WALLPAPER_FADE_OUT_DURATION
+                startDelay = LOCK_WALLPAPER_FADE_OUT_START_DELAY
+                interpolator = Interpolators.LINEAR
+                addUpdateListener { valueAnimator: ValueAnimator ->
+                    setWallpaperAppearAmount(
+                            valueAnimator.animatedValue as Float, closingWallpaperTargets)
+                }
+            }
+        }
+
         with(surfaceBehindEntryAnimator) {
             duration = unlockAnimationDurationMs()
             startDelay = surfaceBehindFadeOutStartDelayMs()
@@ -546,7 +572,8 @@
      */
     fun notifyStartSurfaceBehindRemoteAnimation(
         targets: Array<RemoteAnimationTarget>,
-        wallpapers: Array<RemoteAnimationTarget>,
+        openingWallpapers: Array<RemoteAnimationTarget>,
+        closingWallpapers: Array<RemoteAnimationTarget>,
         startTime: Long,
         requestedShowSurfaceBehindKeyguard: Boolean
     ) {
@@ -556,7 +583,8 @@
         }
 
         surfaceBehindRemoteAnimationTargets = targets
-        wallpaperTargets = wallpapers
+        openingWallpaperTargets = openingWallpapers
+        closingWallpaperTargets = closingWallpapers
         surfaceBehindRemoteAnimationStartTime = startTime
 
         // If we specifically requested that the surface behind be made visible (vs. it being made
@@ -720,8 +748,9 @@
                 return@postDelayed
             }
 
-            if ((wallpaperTargets?.isNotEmpty() == true)) {
+            if ((openingWallpaperTargets?.isNotEmpty() == true)) {
                 fadeInWallpaper()
+                if (fasterUnlockTransition()) fadeOutWallpaper()
                 hideKeyguardViewAfterRemoteAnimation()
             } else {
                 keyguardViewMediator.get().exitKeyguardAndFinishSurfaceBehindRemoteAnimation(
@@ -855,7 +884,8 @@
     /**
      * Scales in and translates up the surface behind the keyguard. This is used during unlock
      * animations and swipe gestures to animate the surface's entry (and exit, if the swipe is
-     * cancelled).
+     * cancelled). When called with [wallpapers]=true, if there are different home and lock screen
+     * wallpapers, this transitions between the two wallpapers
      */
     fun setSurfaceBehindAppearAmount(amount: Float, wallpapers: Boolean = true) {
         val animationAlpha = when {
@@ -923,13 +953,27 @@
         }
 
         if (wallpapers) {
-            setWallpaperAppearAmount(amount)
+            if (!fasterUnlockTransition()) setWallpaperAppearAmount(amount, openingWallpaperTargets)
+            else {
+                // Use the amount to compute the fadeInAmount and fadeOutAmount of the home and lock
+                // screen wallpapers to manually imitate the canned unlock animation.
+                val total = (UNLOCK_ANIMATION_DURATION_MS + CANNED_UNLOCK_START_DELAY).toFloat()
+                val fadeInStart = CANNED_UNLOCK_START_DELAY / total
+                val fadeInAmount = maxOf(0f, (amount - fadeInStart) / (1f - fadeInStart))
+
+                val fadeOutStart = LOCK_WALLPAPER_FADE_OUT_START_DELAY / total
+                val fadeOutEnd = fadeOutStart + LOCK_WALLPAPER_FADE_OUT_DURATION / total
+                val fadeOutAmount = ((amount - fadeOutStart) / (fadeOutEnd - fadeOutStart))
+                        .coerceIn(0f, 1f)
+
+                setWallpaperAppearAmount(fadeInAmount, openingWallpaperTargets)
+                setWallpaperAppearAmount(1 - fadeOutAmount, closingWallpaperTargets)
+            }
         }
     }
 
-    fun setWallpaperAppearAmount(amount: Float) {
+    fun setWallpaperAppearAmount(amount: Float, wallpaperTargets: Array<RemoteAnimationTarget>?) {
         val animationAlpha = amount
-
         wallpaperTargets?.forEach { wallpaper ->
             // SyncRtSurfaceTransactionApplier cannot apply transaction when the target view is
             // unable to draw
@@ -991,7 +1035,8 @@
 
         // That target is no longer valid since the animation finished, null it out.
         surfaceBehindRemoteAnimationTargets = null
-        wallpaperTargets = null
+        openingWallpaperTargets = null
+        if (fasterUnlockTransition()) closingWallpaperTargets = null
 
         playingCannedUnlockAnimation = false
         dismissAmountThresholdsReached = false
@@ -1035,6 +1080,12 @@
         wallpaperCannedUnlockAnimator.start()
     }
 
+    private fun fadeOutWallpaper() {
+        Log.d(TAG, "fadeOutWallpaper")
+        wallpaperFadeOutUnlockAnimator.cancel()
+        wallpaperFadeOutUnlockAnimator.start()
+    }
+
     private fun fadeOutSurfaceBehind() {
         Log.d(TAG, "fadeOutSurfaceBehind")
         surfaceBehindAlphaAnimator.cancel()
@@ -1165,17 +1216,8 @@
      * Temporary method for b/298186160
      * TODO (b/298186160) replace references with the constant itself when flag is removed
      */
-    private fun surfaceBehindFadeOutDurationMs(): Long {
-        return if (fasterUnlockTransition()) SURFACE_BEHIND_FADE_OUT_DURATION_MS
-                else LEGACY_SURFACE_BEHIND_SWIPE_FADE_DURATION_MS
-    }
-
-    /**
-     * Temporary method for b/298186160
-     * TODO (b/298186160) replace references with the constant itself when flag is removed
-     */
     private fun surfaceBehindFadeOutStartDelayMs(): Long {
-        return if (fasterUnlockTransition()) SURFACE_BEHIND_FADE_OUT_START_DELAY_MS
+        return if (fasterUnlockTransition()) UNLOCK_ANIMATION_SURFACE_BEHIND_START_DELAY_MS
                 else LEGACY_UNLOCK_ANIMATION_SURFACE_BEHIND_START_DELAY_MS
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 544bbce..1ea5d1c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -3160,9 +3160,13 @@
                         w -> w.mode == RemoteAnimationTarget.MODE_OPENING).toArray(
                         RemoteAnimationTarget[]::new);
 
+                RemoteAnimationTarget[] closingWallpapers = Arrays.stream(wallpapers).filter(
+                        w -> w.mode == RemoteAnimationTarget.MODE_CLOSING).toArray(
+                        RemoteAnimationTarget[]::new);
+
                 mKeyguardUnlockAnimationControllerLazy.get()
                         .notifyStartSurfaceBehindRemoteAnimation(
-                                openingApps, openingWallpapers, startTime,
+                                openingApps, openingWallpapers, closingWallpapers, startTime,
                                 mSurfaceBehindRemoteAnimationRequested);
             } else {
                 mInteractionJankMonitor.begin(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
index 0863cd7..80675d3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
@@ -28,8 +28,6 @@
     const val CREATE_NOTE = "create_note"
     const val DO_NOT_DISTURB = "do_not_disturb"
     const val FLASHLIGHT = "flashlight"
-    // TODO(b/339667383): delete or properly implement this once a product decision is made
-    const val GLANCEABLE_HUB = "glanceable_hub"
     const val HOME_CONTROLS = "home"
     const val MUTE = "mute"
     const val QR_CODE_SCANNER = "qr_code_scanner"
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt
deleted file mode 100644
index 5d54126..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.data.quickaffordance
-
-import com.android.systemui.Flags
-import com.android.systemui.animation.Expandable
-import com.android.systemui.common.shared.model.ContentDescription
-import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.communal.data.repository.CommunalSceneRepository
-import com.android.systemui.communal.shared.model.CommunalScenes
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.res.R
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOf
-
-/** Shortcut that opens the glanceable hub. */
-// TODO(b/339667383): delete or properly implement this once a product decision is made
-@SysUISingleton
-class GlanceableHubQuickAffordanceConfig
-@Inject
-constructor(
-    private val communalRepository: CommunalSceneRepository,
-) : KeyguardQuickAffordanceConfig {
-
-    override val key: String = BuiltInKeyguardQuickAffordanceKeys.GLANCEABLE_HUB
-
-    override fun pickerName(): String = "Glanceable hub"
-
-    override val pickerIconResourceId = R.drawable.ic_widgets
-
-    override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> by lazy {
-        if (Flags.glanceableHubShortcutButton()) {
-            val contentDescription = ContentDescription.Loaded(pickerName())
-            val icon = Icon.Resource(pickerIconResourceId, contentDescription)
-            flowOf(KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon))
-        } else {
-            flowOf(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
-        }
-    }
-
-    override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
-        return KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
-    }
-
-    override fun onTriggered(
-        expandable: Expandable?
-    ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
-        communalRepository.changeScene(CommunalScenes.Communal, null)
-        return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
index 93296f0..4556195 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
@@ -36,7 +36,6 @@
             camera: CameraQuickAffordanceConfig,
             doNotDisturb: DoNotDisturbQuickAffordanceConfig,
             flashlight: FlashlightQuickAffordanceConfig,
-            glanceableHub: GlanceableHubQuickAffordanceConfig,
             home: HomeControlsKeyguardQuickAffordanceConfig,
             mute: MuteQuickAffordanceConfig,
             quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig,
@@ -47,7 +46,6 @@
                 camera,
                 doNotDisturb,
                 flashlight,
-                glanceableHub,
                 home,
                 mute,
                 quickAccessWallet,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt
index 0748979..796374a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt
@@ -20,7 +20,6 @@
 import android.content.Context
 import android.content.IntentFilter
 import android.content.SharedPreferences
-import com.android.systemui.Flags
 import com.android.systemui.backup.BackupHelper
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
@@ -30,7 +29,6 @@
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
-import com.android.systemui.util.settings.SystemSettings
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.awaitClose
@@ -52,7 +50,6 @@
     @Application private val context: Context,
     private val userFileManager: UserFileManager,
     private val userTracker: UserTracker,
-    private val systemSettings: SystemSettings,
     broadcastDispatcher: BroadcastDispatcher,
 ) : KeyguardQuickAffordanceSelectionManager {
 
@@ -73,22 +70,6 @@
     }
 
     private val defaults: Map<String, List<String>> by lazy {
-        // Quick hack to allow testing out a lock screen shortcut to open the glanceable hub. This
-        // flag will not be rolled out and is only used for local testing.
-        // TODO(b/339667383): delete or properly implement this once a product decision is made
-        if (Flags.glanceableHubShortcutButton()) {
-            if (systemSettings.getBool("open_hub_chip_replace_home_controls", false)) {
-                return@lazy mapOf(
-                    "bottom_start" to listOf("glanceable_hub"),
-                    "bottom_end" to listOf("create_note")
-                )
-            } else {
-                return@lazy mapOf(
-                    "bottom_start" to listOf("home"),
-                    "bottom_end" to listOf("glanceable_hub")
-                )
-            }
-        }
         context.resources
             .getStringArray(R.array.config_keyguardQuickAffordanceDefaults)
             .associate { item ->
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index ab1194e..4f75e6f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -51,7 +51,6 @@
 import com.android.systemui.statusbar.notification.NotificationUtils.interpolate
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
-import com.android.systemui.util.kotlin.Utils.Companion.sampleFilter
 import com.android.systemui.util.kotlin.pairwise
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
@@ -250,17 +249,20 @@
     val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway
 
     /** Keyguard can be clipped at the top as the shade is dragged */
-    val topClippingBounds: Flow<Int?> by lazy {
-        repository.topClippingBounds
-            .sampleFilter(
+    val topClippingBounds: Flow<Int?> =
+        combineTransform(
                 keyguardTransitionInteractor
                     .transitionValue(scene = Scenes.Gone, stateWithoutSceneContainer = GONE)
-                    .onStart { emit(0f) }
-            ) { goneValue ->
-                goneValue != 1f
+                    .map { it == 1f }
+                    .onStart { emit(false) }
+                    .distinctUntilChanged(),
+                repository.topClippingBounds
+            ) { isGone, topClippingBounds ->
+                if (!isGone) {
+                    emit(topClippingBounds)
+                }
             }
             .distinctUntilChanged()
-    }
 
     /** Last point that [KeyguardRootView] view was tapped */
     val lastRootViewTapPosition: Flow<Point?> = repository.lastRootViewTapPosition.asStateFlow()
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
index 846c596..69a157f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
@@ -585,7 +585,8 @@
                                 /* intentSentUiThreadCallback = */ null,
                                 buildLaunchAnimatorController(mMediaViewHolder.getPlayer()),
                                 /* fillIntent = */ null,
-                                /* extraOptions = */ null);
+                                /* extraOptions = */ null,
+                                /* customMessage */ null);
                     } else {
                         try {
                             ActivityOptions opts = ActivityOptions.makeBasic();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
new file mode 100644
index 0000000..007ec3a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalFoundationApi::class)
+
+package com.android.systemui.qs.panels.ui.compose
+
+import android.content.ClipData
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.draganddrop.dragAndDropSource
+import androidx.compose.foundation.draganddrop.dragAndDropTarget
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draganddrop.DragAndDropEvent
+import androidx.compose.ui.draganddrop.DragAndDropTarget
+import androidx.compose.ui.draganddrop.DragAndDropTransferData
+import androidx.compose.ui.draganddrop.mimeTypes
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+@Composable
+fun rememberDragAndDropState(listState: EditTileListState): DragAndDropState {
+    val sourceSpec: MutableState<TileSpec?> = remember { mutableStateOf(null) }
+    return remember(listState) { DragAndDropState(sourceSpec, listState) }
+}
+
+/**
+ * Holds the [TileSpec] of the tile being moved and modify the [EditTileListState] based on drag and
+ * drop events.
+ */
+class DragAndDropState(
+    val sourceSpec: MutableState<TileSpec?>,
+    private val listState: EditTileListState
+) {
+    /** Returns index of the dragged tile if it's present in the list. Returns -1 if not. */
+    fun currentPosition(): Int {
+        return sourceSpec.value?.let { listState.indexOf(it) } ?: -1
+    }
+
+    fun isMoving(tileSpec: TileSpec): Boolean {
+        return sourceSpec.value?.let { it == tileSpec } ?: false
+    }
+
+    fun onStarted(spec: TileSpec) {
+        sourceSpec.value = spec
+    }
+
+    fun onMoved(targetSpec: TileSpec) {
+        sourceSpec.value?.let { listState.move(it, targetSpec) }
+    }
+
+    fun onDrop() {
+        sourceSpec.value = null
+    }
+}
+
+/**
+ * Registers a tile as a [DragAndDropTarget] to receive drag events and update the
+ * [DragAndDropState] with the tile's position, which can be used to insert a temporary placeholder.
+ *
+ * @param dragAndDropState The [DragAndDropState] using the tiles list
+ * @param tileSpec The [TileSpec] of the tile
+ * @param acceptDrops Whether the tile should accept a drop based on a given [TileSpec]
+ * @param onDrop Action to be executed when a [TileSpec] is dropped on the tile
+ */
+@Composable
+fun Modifier.dragAndDropTile(
+    dragAndDropState: DragAndDropState,
+    tileSpec: TileSpec,
+    acceptDrops: (TileSpec) -> Boolean,
+    onDrop: (TileSpec, Int) -> Unit,
+): Modifier {
+    val target =
+        remember(dragAndDropState) {
+            object : DragAndDropTarget {
+                override fun onDrop(event: DragAndDropEvent): Boolean {
+                    return dragAndDropState.sourceSpec.value?.let {
+                        onDrop(it, dragAndDropState.currentPosition())
+                        dragAndDropState.onDrop()
+                        true
+                    } ?: false
+                }
+
+                override fun onEntered(event: DragAndDropEvent) {
+                    dragAndDropState.onMoved(tileSpec)
+                }
+            }
+        }
+    return dragAndDropTarget(
+        shouldStartDragAndDrop = { event ->
+            event.mimeTypes().contains(QsDragAndDrop.TILESPEC_MIME_TYPE) &&
+                dragAndDropState.sourceSpec.value?.let { acceptDrops(it) } ?: false
+        },
+        target = target,
+    )
+}
+
+/**
+ * Registers a tile list as a [DragAndDropTarget] to receive drop events. Use this on list
+ * containers to catch drops outside of tiles.
+ *
+ * @param dragAndDropState The [DragAndDropState] using the tiles list
+ * @param acceptDrops Whether the tile should accept a drop based on a given [TileSpec]
+ * @param onDrop Action to be executed when a [TileSpec] is dropped on the tile
+ */
+@Composable
+fun Modifier.dragAndDropTileList(
+    dragAndDropState: DragAndDropState,
+    acceptDrops: (TileSpec) -> Boolean,
+    onDrop: (TileSpec, Int) -> Unit,
+): Modifier {
+    val target =
+        remember(dragAndDropState) {
+            object : DragAndDropTarget {
+                override fun onDrop(event: DragAndDropEvent): Boolean {
+                    return dragAndDropState.sourceSpec.value?.let {
+                        onDrop(it, dragAndDropState.currentPosition())
+                        dragAndDropState.onDrop()
+                        true
+                    } ?: false
+                }
+            }
+        }
+    return dragAndDropTarget(
+        target = target,
+        shouldStartDragAndDrop = { event ->
+            event.mimeTypes().contains(QsDragAndDrop.TILESPEC_MIME_TYPE) &&
+                dragAndDropState.sourceSpec.value?.let { acceptDrops(it) } ?: false
+        },
+    )
+}
+
+fun Modifier.dragAndDropTileSource(
+    tileSpec: TileSpec,
+    onTap: (TileSpec) -> Unit,
+    dragAndDropState: DragAndDropState
+): Modifier {
+    return dragAndDropSource {
+        detectTapGestures(
+            onTap = { onTap(tileSpec) },
+            onLongPress = {
+                dragAndDropState.onStarted(tileSpec)
+
+                // The tilespec from the ClipData transferred isn't actually needed as we're moving
+                // a tile within the same application. We're using a custom MIME type to limit the
+                // drag event to QS.
+                startTransfer(
+                    DragAndDropTransferData(
+                        ClipData(
+                            QsDragAndDrop.CLIPDATA_LABEL,
+                            arrayOf(QsDragAndDrop.TILESPEC_MIME_TYPE),
+                            ClipData.Item(tileSpec.spec)
+                        )
+                    )
+                )
+            }
+        )
+    }
+}
+
+private object QsDragAndDrop {
+    const val CLIPDATA_LABEL = "tilespec"
+    const val TILESPEC_MIME_TYPE = "qstile/tilespec"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
new file mode 100644
index 0000000..482c498
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.qs.panels.ui.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.runtime.toMutableStateList
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+@Composable
+fun rememberEditListState(
+    tiles: List<EditTileViewModel>,
+): EditTileListState {
+    return remember(tiles) { EditTileListState(tiles) }
+}
+
+/** Holds the temporary state of the tile list during a drag movement where we move tiles around. */
+class EditTileListState(tiles: List<EditTileViewModel>) {
+    val tiles: SnapshotStateList<EditTileViewModel> = tiles.toMutableStateList()
+
+    fun move(tileSpec: TileSpec, target: TileSpec) {
+        val fromIndex = indexOf(tileSpec)
+        val toIndex = indexOf(target)
+
+        if (fromIndex == -1 || toIndex == -1 || fromIndex == toIndex) {
+            return
+        }
+
+        val isMovingToCurrent = tiles[toIndex].isCurrent
+        tiles.apply { add(toIndex, removeAt(fromIndex).copy(isCurrent = isMovingToCurrent)) }
+    }
+
+    fun indexOf(tileSpec: TileSpec): Int {
+        return tiles.indexOfFirst { it.tileSpec == tileSpec }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
index 7f5e474..092ad44 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
@@ -110,12 +110,15 @@
         tiles: List<EditTileViewModel>,
         modifier: Modifier,
         onAddTile: (TileSpec, Int) -> Unit,
-        onRemoveTile: (TileSpec) -> Unit
+        onRemoveTile: (TileSpec) -> Unit,
     ) {
         val columns by viewModel.columns.collectAsStateWithLifecycle()
         val showLabels by viewModel.showLabels.collectAsStateWithLifecycle()
 
-        val (currentTiles, otherTiles) = tiles.partition { it.isCurrent }
+        val listState = rememberEditListState(tiles)
+        val dragAndDropState = rememberDragAndDropState(listState)
+
+        val (currentTiles, otherTiles) = listState.tiles.partition { it.isCurrent }
         val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
             onAddTile(it, CurrentTilesInteractor.POSITION_AT_END)
         }
@@ -156,20 +159,24 @@
                 largeTileHeight = largeTileHeight,
                 iconTileHeight = iconTileHeight,
                 tilePadding = tilePadding,
-                onRemoveTile = onRemoveTile,
+                onAdd = onAddTile,
+                onRemove = onRemoveTile,
                 isIconOnly = viewModel::isIconTile,
                 columns = columns,
                 showLabels = showLabels,
+                dragAndDropState = dragAndDropState,
             )
             AvailableTiles(
-                tiles = otherTiles,
+                tiles = otherTiles.filter { !dragAndDropState.isMoving(it.tileSpec) },
                 largeTileHeight = largeTileHeight,
                 iconTileHeight = iconTileHeight,
                 tilePadding = tilePadding,
                 addTileToEnd = addTileToEnd,
+                onRemove = onRemoveTile,
                 isIconOnly = viewModel::isIconTile,
                 showLabels = showLabels,
                 columns = columns,
+                dragAndDropState = dragAndDropState,
             )
         }
     }
@@ -194,10 +201,12 @@
         largeTileHeight: Dp,
         iconTileHeight: Dp,
         tilePadding: Dp,
-        onRemoveTile: (TileSpec) -> Unit,
+        onAdd: (TileSpec, Int) -> Unit,
+        onRemove: (TileSpec) -> Unit,
         isIconOnly: (TileSpec) -> Boolean,
         showLabels: Boolean,
         columns: Int,
+        dragAndDropState: DragAndDropState,
     ) {
         val (smallTiles, largeTiles) = tiles.partition { isIconOnly(it.tileSpec) }
 
@@ -207,29 +216,40 @@
         CurrentTilesContainer {
             TileLazyGrid(
                 columns = GridCells.Fixed(columns),
-                modifier = Modifier.height(largeGridHeight),
+                modifier =
+                    Modifier.height(largeGridHeight)
+                        .dragAndDropTileList(dragAndDropState, { !isIconOnly(it) }, onAdd)
             ) {
                 editTiles(
-                    largeTiles,
-                    ClickAction.REMOVE,
-                    onRemoveTile,
-                    { false },
-                    indicatePosition = true
+                    tiles = largeTiles,
+                    clickAction = ClickAction.REMOVE,
+                    onClick = onRemove,
+                    isIconOnly = { false },
+                    dragAndDropState = dragAndDropState,
+                    acceptDrops = { !isIconOnly(it) },
+                    onDrop = onAdd,
+                    indicatePosition = true,
                 )
             }
         }
+
         CurrentTilesContainer {
             TileLazyGrid(
                 columns = GridCells.Fixed(columns),
-                modifier = Modifier.height(smallGridHeight),
+                modifier =
+                    Modifier.height(smallGridHeight)
+                        .dragAndDropTileList(dragAndDropState, { isIconOnly(it) }, onAdd)
             ) {
                 editTiles(
-                    smallTiles,
-                    ClickAction.REMOVE,
-                    onRemoveTile,
-                    { true },
+                    tiles = smallTiles,
+                    clickAction = ClickAction.REMOVE,
+                    onClick = onRemove,
+                    isIconOnly = { true },
                     showLabels = showLabels,
-                    indicatePosition = true
+                    dragAndDropState = dragAndDropState,
+                    acceptDrops = { isIconOnly(it) },
+                    onDrop = onAdd,
+                    indicatePosition = true,
                 )
             }
         }
@@ -242,9 +262,11 @@
         iconTileHeight: Dp,
         tilePadding: Dp,
         addTileToEnd: (TileSpec) -> Unit,
+        onRemove: (TileSpec) -> Unit,
         isIconOnly: (TileSpec) -> Boolean,
         showLabels: Boolean,
         columns: Int,
+        dragAndDropState: DragAndDropState,
     ) {
         val (tilesStock, tilesCustom) = tiles.partition { it.appName == null }
         val (smallTiles, largeTiles) = tilesStock.partition { isIconOnly(it.tileSpec) }
@@ -258,13 +280,27 @@
         val gridHeight =
             largeGridHeight + smallGridHeight + largeGridHeightCustom + (tilePadding * 2)
 
+        val onDrop: (TileSpec, Int) -> Unit by rememberUpdatedState { tileSpec, _ ->
+            onRemove(tileSpec)
+        }
+
         AvailableTilesContainer {
             TileLazyGrid(
                 columns = GridCells.Fixed(columns),
-                modifier = Modifier.height(gridHeight),
+                modifier =
+                    Modifier.height(gridHeight)
+                        .dragAndDropTileList(dragAndDropState, { true }, onDrop)
             ) {
                 // Large tiles
-                editTiles(largeTiles, ClickAction.ADD, addTileToEnd, isIconOnly)
+                editTiles(
+                    largeTiles,
+                    ClickAction.ADD,
+                    addTileToEnd,
+                    isIconOnly,
+                    dragAndDropState,
+                    acceptDrops = { true },
+                    onDrop = onDrop,
+                )
                 fillUpRow(nTiles = largeTiles.size, columns = columns / 2)
 
                 // Small tiles
@@ -273,7 +309,10 @@
                     ClickAction.ADD,
                     addTileToEnd,
                     isIconOnly,
-                    showLabels = showLabels
+                    showLabels = showLabels,
+                    dragAndDropState = dragAndDropState,
+                    acceptDrops = { true },
+                    onDrop = onDrop,
                 )
                 fillUpRow(nTiles = smallTiles.size, columns = columns)
 
@@ -283,7 +322,10 @@
                     ClickAction.ADD,
                     addTileToEnd,
                     isIconOnly,
-                    showLabels = showLabels
+                    showLabels = showLabels,
+                    dragAndDropState = dragAndDropState,
+                    acceptDrops = { true },
+                    onDrop = onDrop,
                 )
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
index bbb98d3..0bb4cfa 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
@@ -74,12 +74,12 @@
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.animation.Expandable
+import com.android.compose.modifiers.thenIf
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.ui.compose.Icon
 import com.android.systemui.common.ui.compose.load
 import com.android.systemui.plugins.qs.QSTile
-import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
@@ -114,6 +114,7 @@
         showLabels = showLabels,
         label = state.label.toString(),
         iconOnly = iconOnly,
+        clickEnabled = true,
         onClick = tile::onClick,
         onLongClick = tile::onLongClick,
         modifier = modifier,
@@ -127,6 +128,7 @@
                 secondaryLabel = state.secondaryLabel.toString(),
                 icon = icon,
                 colors = colors,
+                clickEnabled = true,
                 onClick = tile::onSecondaryClick,
                 onLongClick = tile::onLongClick,
             )
@@ -140,7 +142,7 @@
     showLabels: Boolean,
     label: String,
     iconOnly: Boolean,
-    clickEnabled: Boolean = true,
+    clickEnabled: Boolean = false,
     onClick: (Expandable) -> Unit = {},
     onLongClick: (Expandable) -> Unit = {},
     modifier: Modifier = Modifier,
@@ -168,11 +170,12 @@
             Box(
                 modifier =
                     Modifier.fillMaxSize()
-                        .combinedClickable(
-                            enabled = clickEnabled,
-                            onClick = { onClick(it) },
-                            onLongClick = { onLongClick(it) }
-                        )
+                        .thenIf(clickEnabled) {
+                            Modifier.combinedClickable(
+                                onClick = { onClick(it) },
+                                onLongClick = { onLongClick(it) }
+                            )
+                        }
                         .tilePadding(),
             ) {
                 content()
@@ -197,7 +200,7 @@
     secondaryLabel: String?,
     icon: Icon,
     colors: TileColors,
-    clickEnabled: Boolean = true,
+    clickEnabled: Boolean = false,
     onClick: (Expandable) -> Unit = {},
     onLongClick: (Expandable) -> Unit = {},
 ) {
@@ -212,13 +215,12 @@
         ) {
             Box(
                 modifier =
-                    Modifier.fillMaxSize()
-                        .clip(TileDefaults.TileShape)
-                        .combinedClickable(
-                            enabled = clickEnabled,
+                    Modifier.fillMaxSize().clip(TileDefaults.TileShape).thenIf(clickEnabled) {
+                        Modifier.combinedClickable(
                             onClick = { onClick(it) },
                             onLongClick = { onLongClick(it) }
                         )
+                    }
             ) {
                 TileIcon(
                     icon = icon,
@@ -269,13 +271,29 @@
     onAddTile: (TileSpec, Int) -> Unit,
     onRemoveTile: (TileSpec) -> Unit,
 ) {
-    val (currentTiles, otherTiles) = tiles.partition { it.isCurrent }
-    val (otherTilesStock, otherTilesCustom) = otherTiles.partition { it.appName == null }
+    val currentListState = rememberEditListState(tiles)
+    val dragAndDropState = rememberDragAndDropState(currentListState)
+
+    val (currentTiles, otherTiles) = currentListState.tiles.partition { it.isCurrent }
+    val (otherTilesStock, otherTilesCustom) =
+        otherTiles
+            .filter { !dragAndDropState.isMoving(it.tileSpec) }
+            .partition { it.appName == null }
     val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
         onAddTile(it, CurrentTilesInteractor.POSITION_AT_END)
     }
 
-    TileLazyGrid(modifier = modifier, columns = columns) {
+    val onDropAdd: (TileSpec, Int) -> Unit by rememberUpdatedState { tileSpec, position ->
+        onAddTile(tileSpec, position)
+    }
+    val onDropRemove: (TileSpec, Int) -> Unit by rememberUpdatedState { tileSpec, _ ->
+        onRemoveTile(tileSpec)
+    }
+
+    TileLazyGrid(
+        modifier = modifier.dragAndDropTileList(dragAndDropState, { true }, onDropAdd),
+        columns = columns
+    ) {
         // These Text are just placeholders to see the different sections. Not final UI.
         item(span = { GridItemSpan(maxLineSpan) }) { Text("Current tiles", color = Color.White) }
 
@@ -285,6 +303,9 @@
             onRemoveTile,
             isIconOnly,
             indicatePosition = true,
+            dragAndDropState = dragAndDropState,
+            acceptDrops = { true },
+            onDrop = onDropAdd,
         )
 
         item(span = { GridItemSpan(maxLineSpan) }) { Text("Tiles to add", color = Color.White) }
@@ -294,6 +315,9 @@
             ClickAction.ADD,
             addTileToEnd,
             isIconOnly,
+            dragAndDropState = dragAndDropState,
+            acceptDrops = { true },
+            onDrop = onDropRemove,
         )
 
         item(span = { GridItemSpan(maxLineSpan) }) {
@@ -305,6 +329,9 @@
             ClickAction.ADD,
             addTileToEnd,
             isIconOnly,
+            dragAndDropState = dragAndDropState,
+            acceptDrops = { true },
+            onDrop = onDropRemove,
         )
     }
 }
@@ -314,6 +341,9 @@
     clickAction: ClickAction,
     onClick: (TileSpec) -> Unit,
     isIconOnly: (TileSpec) -> Boolean,
+    dragAndDropState: DragAndDropState,
+    acceptDrops: (TileSpec) -> Boolean,
+    onDrop: (TileSpec, Int) -> Unit,
     showLabels: Boolean = false,
     indicatePosition: Boolean = false,
 ) {
@@ -322,41 +352,44 @@
         key = { tiles[it].tileSpec.spec },
         span = { GridItemSpan(if (isIconOnly(tiles[it].tileSpec)) 1 else 2) },
         contentType = { TileType }
-    ) {
-        val viewModel = tiles[it]
-        val canClick =
-            when (clickAction) {
-                ClickAction.ADD -> AvailableEditActions.ADD in viewModel.availableEditActions
-                ClickAction.REMOVE -> AvailableEditActions.REMOVE in viewModel.availableEditActions
-            }
-        val onClickActionName =
-            when (clickAction) {
-                ClickAction.ADD ->
-                    stringResource(id = R.string.accessibility_qs_edit_tile_add_action)
-                ClickAction.REMOVE ->
-                    stringResource(id = R.string.accessibility_qs_edit_remove_tile_action)
-            }
-        val stateDescription =
-            if (indicatePosition) {
-                stringResource(id = R.string.accessibility_qs_edit_position, it + 1)
-            } else {
-                ""
-            }
-
+    ) { index ->
+        val viewModel = tiles[index]
         val iconOnly = isIconOnly(viewModel.tileSpec)
         val tileHeight = tileHeight(iconOnly && showLabels)
-        EditTile(
-            tileViewModel = viewModel,
-            iconOnly = iconOnly,
-            showLabels = showLabels,
-            clickEnabled = canClick,
-            onClick = { onClick.invoke(viewModel.tileSpec) },
-            modifier =
-                Modifier.height(tileHeight).animateItem().semantics {
-                    onClick(onClickActionName) { false }
-                    this.stateDescription = stateDescription
+
+        if (!dragAndDropState.isMoving(viewModel.tileSpec)) {
+            val onClickActionName =
+                when (clickAction) {
+                    ClickAction.ADD ->
+                        stringResource(id = R.string.accessibility_qs_edit_tile_add_action)
+                    ClickAction.REMOVE ->
+                        stringResource(id = R.string.accessibility_qs_edit_remove_tile_action)
                 }
-        )
+            val stateDescription =
+                if (indicatePosition) {
+                    stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
+                } else {
+                    ""
+                }
+            EditTile(
+                tileViewModel = viewModel,
+                iconOnly = iconOnly,
+                showLabels = showLabels,
+                modifier =
+                    Modifier.height(tileHeight)
+                        .animateItem()
+                        .semantics {
+                            onClick(onClickActionName) { false }
+                            this.stateDescription = stateDescription
+                        }
+                        .dragAndDropTile(dragAndDropState, viewModel.tileSpec, acceptDrops, onDrop)
+                        .dragAndDropTileSource(
+                            viewModel.tileSpec,
+                            onClick,
+                            dragAndDropState,
+                        )
+            )
+        }
     }
 }
 
@@ -365,8 +398,6 @@
     tileViewModel: EditTileViewModel,
     iconOnly: Boolean,
     showLabels: Boolean,
-    clickEnabled: Boolean,
-    onClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
     val label = tileViewModel.label.load() ?: tileViewModel.tileSpec.spec
@@ -377,9 +408,6 @@
         showLabels = showLabels,
         label = label,
         iconOnly = iconOnly,
-        clickEnabled = clickEnabled,
-        onClick = { onClick() },
-        onLongClick = { onClick() },
         modifier = modifier,
     ) {
         if (iconOnly) {
@@ -394,9 +422,6 @@
                 secondaryLabel = tileViewModel.appName?.load(),
                 icon = tileViewModel.icon,
                 colors = colors,
-                clickEnabled = clickEnabled,
-                onClick = { onClick() },
-                onLongClick = { onClick() },
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
index ba9a044..a4c8638 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
@@ -26,7 +26,7 @@
  * [isCurrent] indicates whether this tile is part of the current set of tiles that the user sees in
  * Quick Settings.
  */
-class EditTileViewModel(
+data class EditTileViewModel(
     val tileSpec: TileSpec,
     val icon: Icon,
     val label: Text,
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 23faf7d..4b82e0f 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -62,6 +62,7 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.util.Log;
 import android.view.InputDevice;
 import android.view.KeyCharacterMap;
@@ -177,7 +178,7 @@
     private boolean mBound;
     private boolean mIsEnabled;
 
-    private boolean mIsNonPrimaryUser;
+    private boolean mIsSystemOrVisibleBgUser;
     private int mCurrentBoundedUserId = -1;
     private boolean mInputFocusTransferStarted;
     private float mInputFocusTransferStartY;
@@ -629,6 +630,7 @@
             SysUiState sysUiState,
             Provider<SceneInteractor> sceneInteractor,
             UserTracker userTracker,
+            UserManager userManager,
             WakefulnessLifecycle wakefulnessLifecycle,
             UiEventLogger uiEventLogger,
             DisplayTracker displayTracker,
@@ -639,10 +641,18 @@
             Optional<UnfoldTransitionProgressForwarder> unfoldTransitionProgressForwarder,
             BroadcastDispatcher broadcastDispatcher
     ) {
-        // b/241601880: This component shouldn't be running for a non-primary user
-        mIsNonPrimaryUser = !Process.myUserHandle().equals(UserHandle.SYSTEM);
-        if (mIsNonPrimaryUser) {
-            Log.wtf(TAG_OPS, "Unexpected initialization for non-primary user", new Throwable());
+        // b/241601880: This component should only be running for primary users or
+        // secondaryUsers when visibleBackgroundUsers are supported.
+        boolean isSystemUser = Process.myUserHandle().equals(UserHandle.SYSTEM);
+        boolean isVisibleBackgroundUser =
+                userManager.isVisibleBackgroundUsersSupported() && !userManager.isUserForeground();
+        if (!isSystemUser && isVisibleBackgroundUser) {
+            Log.d(TAG_OPS, "Initialization for visibleBackgroundUser");
+        }
+        mIsSystemOrVisibleBgUser = isSystemUser || isVisibleBackgroundUser;
+        if (!mIsSystemOrVisibleBgUser) {
+            Log.wtf(TAG_OPS, "Unexpected initialization for non-system foreground user",
+                    new Throwable());
         }
 
         mContext = context;
@@ -833,11 +843,12 @@
     }
 
     private void internalConnectToCurrentUser(String reason) {
-        if (mIsNonPrimaryUser) {
+        if (!mIsSystemOrVisibleBgUser) {
             // This should not happen, but if any per-user SysUI component has a dependency on OPS,
             // then this could get triggered
-            Log.w(TAG_OPS, "Skipping connection to overview service due to non-primary user "
-                    + "caller");
+            Log.w(TAG_OPS,
+                    "Skipping connection to overview service due to non-system foreground user "
+                            + "caller");
             return;
         }
         disconnectFromLauncherService(reason);
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 25a9e9e..ef01194 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -167,6 +167,10 @@
                 initialValue = isVisibleInternal()
             )
 
+    /** Whether there's an ongoing remotely-initiated user interaction. */
+    val isRemoteUserInteractionOngoing: StateFlow<Boolean> =
+        repository.isRemoteUserInteractionOngoing
+
     /**
      * The amount of transition into or out of the given [scene].
      *
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index d380251..a28222e 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -26,17 +26,12 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.model.Scene
 import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
 import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
 
 /** Models UI state for the scene container. */
 @SysUISingleton
@@ -46,7 +41,6 @@
     private val sceneInteractor: SceneInteractor,
     private val falsingInteractor: FalsingInteractor,
     private val powerInteractor: PowerInteractor,
-    scenes: Set<@JvmSuppressWildcards Scene>,
 ) {
     /**
      * Keys of all scenes in the container.
@@ -62,25 +56,6 @@
     /** Whether the container is visible. */
     val isVisible: StateFlow<Boolean> = sceneInteractor.isVisible
 
-    private val destinationScenesBySceneKey =
-        scenes.associate { scene ->
-            scene.key to scene.destinationScenes.flatMapLatestConflated { replaceSceneFamilies(it) }
-        }
-
-    fun currentDestinationScenes(
-        scope: CoroutineScope,
-    ): StateFlow<Map<UserAction, UserActionResult>> {
-        return currentScene
-            .flatMapLatestConflated { currentSceneKey ->
-                checkNotNull(destinationScenesBySceneKey[currentSceneKey])
-            }
-            .stateIn(
-                scope = scope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = emptyMap(),
-            )
-    }
-
     /**
      * Binds the given flow so the system remembers it.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java
index ab4480d..c4f6cd9 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java
@@ -23,12 +23,14 @@
 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.ACTION_FINISH_FROM_TRAMPOLINE;
 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_CALLING_PACKAGE_NAME;
 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_CALLING_PACKAGE_TASK_ID;
+import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_CLIP_DATA;
 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_RESULT_RECEIVER;
 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI;
 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.PERMISSION_SELF;
 
 import android.app.Activity;
 import android.content.BroadcastReceiver;
+import android.content.ClipData;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -299,6 +301,14 @@
         data.putInt(Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE,
                 Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
         data.putParcelable(EXTRA_SCREENSHOT_URI, uri);
+
+        if (mBacklinksIncludeDataCheckBox.getVisibility() == View.VISIBLE
+                && mBacklinksIncludeDataCheckBox.isChecked()
+                && mViewModel.getBacklinksLiveData().getValue() != null) {
+            ClipData backlinksData = mViewModel.getBacklinksLiveData().getValue().getClipData();
+            data.putParcelable(EXTRA_CLIP_DATA, backlinksData);
+        }
+
         try {
             mResultReceiver.send(Activity.RESULT_OK, data);
             logUiEvent(SCREENSHOT_FOR_NOTE_ACCEPTED);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java
index 3c4469d..0161f78 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java
@@ -26,6 +26,7 @@
 
 import android.app.Activity;
 import android.content.ActivityNotFoundException;
+import android.content.ClipData;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -82,6 +83,7 @@
     private static final String TAG = AppClipsTrampolineActivity.class.getSimpleName();
     static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
     static final String EXTRA_SCREENSHOT_URI = TAG + "SCREENSHOT_URI";
+    static final String EXTRA_CLIP_DATA = TAG + "CLIP_DATA";
     static final String ACTION_FINISH_FROM_TRAMPOLINE = TAG + "FINISH_FROM_TRAMPOLINE";
     static final String EXTRA_RESULT_RECEIVER = TAG + "RESULT_RECEIVER";
     static final String EXTRA_CALLING_PACKAGE_NAME = TAG + "CALLING_PACKAGE_NAME";
@@ -265,6 +267,11 @@
                 convertedData.setData(uri);
             }
 
+            if (resultData.containsKey(EXTRA_CLIP_DATA)) {
+                ClipData backlinksData = resultData.getParcelable(EXTRA_CLIP_DATA, ClipData.class);
+                convertedData.setClipData(backlinksData);
+            }
+
             // Broadcast no longer required, setting it to null.
             mKillAppClipsBroadcastIntent = null;
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 16d10ab..6b4e44f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -1095,7 +1095,6 @@
         initBottomArea();
 
         mWakeUpCoordinator.setStackScroller(mNotificationStackScrollLayoutController);
-        mPulseExpansionHandler.setUp(mNotificationStackScrollLayoutController);
         mWakeUpCoordinator.addListener(new NotificationWakeUpCoordinator.WakeUpListener() {
             @Override
             public void onFullyHiddenChanged(boolean isFullyHidden) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index ce321dc..5065baa 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -119,7 +119,7 @@
             if (delayed) {
                 scope.launch {
                     delay(125)
-                    animateCollapseShadeInternal()
+                    withContext(mainDispatcher) { animateCollapseShadeInternal() }
                 }
             } else {
                 animateCollapseShadeInternal()
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
index 3f4bcba..354d379 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
@@ -32,6 +32,8 @@
 import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.shade.transition.ScrimShadeTransitionController
+import com.android.systemui.statusbar.PulseExpansionHandler
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.policy.SplitShadeStateController
 import javax.inject.Inject
 import javax.inject.Provider
@@ -56,6 +58,8 @@
     private val sceneInteractorProvider: Provider<SceneInteractor>,
     private val panelExpansionInteractorProvider: Provider<PanelExpansionInteractor>,
     private val shadeExpansionStateManager: ShadeExpansionStateManager,
+    private val pulseExpansionHandler: PulseExpansionHandler,
+    private val nsslc: NotificationStackScrollLayoutController,
 ) : CoreStartable {
 
     override fun start() {
@@ -63,6 +67,7 @@
         hydrateShadeExpansionStateManager()
         logTouchesTo(touchLog)
         scrimShadeTransitionController.init()
+        pulseExpansionHandler.setUp(nsslc)
     }
 
     private fun hydrateShadeExpansionStateManager() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 2f3fc729..c1eb8bc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -1276,7 +1276,7 @@
             mPowerPluggedInWired = status.isPluggedInWired() && isChargingOrFull;
             mPowerPluggedInWireless = status.isPluggedInWireless() && isChargingOrFull;
             mPowerPluggedInDock = status.isPluggedInDock() && isChargingOrFull;
-            mPowerPluggedIn = status.isPluggedIn() && isChargingOrFull;
+            mPowerPluggedIn = isPowerPluggedIn(status, isChargingOrFull);
             mPowerCharged = status.isCharged();
             mChargingWattage = status.maxChargingWattage;
             mChargingSpeed = status.getChargingSpeed(mContext);
@@ -1562,6 +1562,11 @@
         return status.isBatteryDefender();
     }
 
+    /** Return true if the device has power plugged in. */
+    protected boolean isPowerPluggedIn(BatteryStatus status, boolean isChargingOrFull) {
+        return status.isPluggedIn() && isChargingOrFull;
+    }
+
     private boolean isCurrentUser(int userId) {
         return getCurrentUser() == userId;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
index 367aaad..48c89f8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
@@ -22,19 +22,25 @@
 import android.app.Notification.CATEGORY_EVENT
 import android.app.Notification.CATEGORY_REMINDER
 import android.app.Notification.VISIBILITY_PRIVATE
+import android.app.NotificationManager
 import android.app.NotificationManager.IMPORTANCE_DEFAULT
 import android.app.NotificationManager.IMPORTANCE_HIGH
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.PERMISSION_GRANTED
 import android.database.ContentObserver
 import android.hardware.display.AmbientDisplayConfiguration
 import android.os.Handler
 import android.os.PowerManager
+import android.os.SystemProperties
 import android.provider.Settings
 import android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED
 import android.provider.Settings.Global.HEADS_UP_OFF
 import com.android.internal.logging.UiEvent
 import com.android.internal.logging.UiEventLogger
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.settings.UserTracker
@@ -47,6 +53,7 @@
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.HeadsUpManager
+import com.android.systemui.util.NotificationChannels
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.settings.SystemSettings
 import com.android.systemui.util.time.SystemClock
@@ -244,12 +251,22 @@
         keyguardNotificationVisibilityProvider.shouldHideNotification(entry)
 }
 
+/**
+ * Set with:
+ * adb shell setprop persist.force_show_avalanche_edu_once 1 && adb shell stop; adb shell start
+ */
+private const val FORCE_SHOW_AVALANCHE_EDU_ONCE = "persist.force_show_avalanche_edu_once"
+
+private const val PREF_HAS_SEEN_AVALANCHE_EDU = "has_seen_avalanche_edu"
+
 class AvalancheSuppressor(
     private val avalancheProvider: AvalancheProvider,
     private val systemClock: SystemClock,
     private val systemSettings: SystemSettings,
     private val packageManager: PackageManager,
     private val uiEventLogger: UiEventLogger,
+    private val context: Context,
+    private val notificationManager: NotificationManager
 ) :
     VisualInterruptionFilter(
         types = setOf(PEEK, PULSE),
@@ -257,6 +274,24 @@
     ) {
     val TAG = "AvalancheSuppressor"
 
+    private val prefs = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
+
+    // SharedPreferences are persisted across reboots
+    var hasSeenEdu: Boolean
+        get() = prefs.getBoolean(PREF_HAS_SEEN_AVALANCHE_EDU, false)
+        set(value) = prefs.edit().putBoolean(PREF_HAS_SEEN_AVALANCHE_EDU, value).apply()
+
+    // Reset on reboot.
+    // The pipeline runs these suppressors many times very fast, so we must use a separate bool
+    // to force show for debug so that phone does not get stuck sending out infinite number of
+    // education HUNs.
+    private var hasShownOnceForDebug = false
+
+    private fun shouldShowEdu() : Boolean {
+        val forceShowOnce = SystemProperties.get(FORCE_SHOW_AVALANCHE_EDU_ONCE, "").equals("1")
+        return !hasSeenEdu || (forceShowOnce && !hasShownOnceForDebug)
+    }
+
     enum class State {
         ALLOW_CONVERSATION_AFTER_AVALANCHE,
         ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME,
@@ -309,9 +344,46 @@
         if (state != State.SUPPRESS) {
             return false
         }
+        if (shouldShowEdu()) {
+            showEdu()
+        }
         return true
     }
 
+    /**
+     * Show avalanche education HUN from SystemUI.
+     */
+    private fun showEdu() {
+        val res = context.resources
+        val titleStr = res.getString(
+            com.android.systemui.res.R.string.adaptive_notification_edu_hun_title)
+        val textStr = res.getString(
+            com.android.systemui.res.R.string.adaptive_notification_edu_hun_text)
+        val actionStr = res.getString(
+            com.android.systemui.res.R.string.go_to_adaptive_notification_settings)
+
+        val intent = Intent(Settings.ACTION_MANAGE_ADAPTIVE_NOTIFICATIONS)
+        val pendingIntent = PendingIntent.getActivity(
+            context, 0, intent,
+            PendingIntent.FLAG_IMMUTABLE
+        )
+
+        val builder =
+            Notification.Builder(context, NotificationChannels.ALERTS)
+                .setTicker(titleStr)
+                .setContentTitle(titleStr)
+                .setContentText(textStr)
+                .setSmallIcon(com.android.systemui.res.R.drawable.ic_settings)
+                .setCategory(Notification.CATEGORY_SYSTEM)
+                .setAutoCancel(true)
+                .addAction(android.R.drawable.button_onoff_indicator_off, actionStr, pendingIntent)
+                .setContentIntent(pendingIntent)
+
+        notificationManager.notify(SystemMessage.NOTE_ADAPTIVE_NOTIFICATIONS, builder.build())
+        hasSeenEdu = true
+        hasShownOnceForDebug = true;
+    }
+
     private fun calculateState(entry: NotificationEntry): State {
         if (
             entry.ranking.isConversation &&
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
index 84f8662..96f94ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.statusbar.notification.interruption
 
+import android.app.NotificationManager
+import android.content.Context
 import android.content.pm.PackageManager
 import android.hardware.display.AmbientDisplayConfiguration
 import android.os.Handler
@@ -68,7 +70,9 @@
     private val avalancheProvider: AvalancheProvider,
     private val systemSettings: SystemSettings,
     private val packageManager: PackageManager,
-    private val bubbles: Optional<Bubbles>
+    private val bubbles: Optional<Bubbles>,
+    private val context: Context,
+    private val notificationManager: NotificationManager
 ) : VisualInterruptionDecisionProvider {
 
     init {
@@ -179,7 +183,7 @@
         if (NotificationAvalancheSuppression.isEnabled) {
             addFilter(
                 AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                        uiEventLogger)
+                        uiEventLogger, context, notificationManager)
             )
             avalancheProvider.register()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index f73223f..4a043d3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -2869,14 +2869,16 @@
     }
 
     public boolean isExpanded(boolean allowOnKeyguard) {
-        // System expanded should be ignored in heads up state
         final boolean isHeadsUpState = ExpandHeadsUpOnInlineReply.isEnabled()
                 && canShowHeadsUp() && isHeadsUpState();
+        // System expanded should be ignored in pinned heads up state
+        final boolean isPinned = isHeadsUpState && isPinned();
         // Heads Up Notification can be expanded when it is pinned.
         final boolean isPinnedAndExpanded =
                 isHeadsUpState && isPinnedAndExpanded();
+
         return (!shouldShowPublic()) && (!mOnKeyguard || allowOnKeyguard)
-                && (!hasUserChangedExpansion() && !isHeadsUpState
+                && (!hasUserChangedExpansion() && !isPinned
                 && (isSystemExpanded() || isSystemChildExpanded())
                 || isUserExpanded() || isPinnedAndExpanded);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index ebb0d7d..57e52b7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -70,10 +70,10 @@
             ) { shadeExpansion, shadeMode, qsExpansion, transitionState, quickSettingsScene ->
                 when (transitionState) {
                     is ObservableTransitionState.Idle -> {
-                        if (transitionState.currentScene == Scenes.Lockscreen) {
-                            1f
-                        } else {
-                            shadeExpansion
+                        when (transitionState.currentScene) {
+                            Scenes.Lockscreen,
+                            Scenes.QuickSettings -> 1f
+                            else -> shadeExpansion
                         }
                     }
                     is ObservableTransitionState.Transition -> {
@@ -162,9 +162,13 @@
         stackAppearanceInteractor::setCurrentGestureOverscroll
 
     /** Whether the notification stack is scrollable or not. */
-    val isScrollable: Flow<Boolean> = sceneInteractor.currentScene.map {
-        sceneInteractor.isSceneInFamily(it, SceneFamilies.NotifShade) || it == Scenes.Lockscreen
-    }.dumpWhileCollecting("isScrollable")
+    val isScrollable: Flow<Boolean> =
+        sceneInteractor.currentScene
+            .map {
+                sceneInteractor.isSceneInFamily(it, SceneFamilies.NotifShade) ||
+                    it == Scenes.Lockscreen
+            }
+            .dumpWhileCollecting("isScrollable")
 
     /** Whether the notification stack is displayed in doze mode. */
     val isDozing: Flow<Boolean> by lazy {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
index fae0a46..97266c5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
@@ -126,6 +126,7 @@
         animationController: ActivityTransitionAnimator.Controller?,
         fillInIntent: Intent?,
         extraOptions: Bundle?,
+        customMessage: String?,
     ) {
         activityStarterInternal.startPendingIntentDismissingKeyguard(
             intent = intent,
@@ -135,6 +136,7 @@
             dismissShade = dismissShade,
             fillInIntent = fillInIntent,
             extraOptions = extraOptions,
+            customMessage = customMessage,
         )
     }
 
@@ -319,11 +321,13 @@
         intent: Intent,
         onlyProvisioned: Boolean,
         dismissShade: Boolean,
+        customMessage: String?,
     ) {
         activityStarterInternal.startActivityDismissingKeyguard(
             intent = intent,
             onlyProvisioned = onlyProvisioned,
             dismissShade = dismissShade,
+            customMessage = customMessage,
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternal.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternal.kt
index cff9f5e..93ce6e8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternal.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternal.kt
@@ -42,6 +42,7 @@
         skipLockscreenChecks: Boolean = false,
         fillInIntent: Intent? = null,
         extraOptions: Bundle? = null,
+        customMessage: String? = null,
     )
 
     /** Starts an activity after dismissing keyguard. */
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 dbb95e6..ae98e1d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
@@ -42,7 +42,8 @@
         showOverLockscreen: Boolean,
         skipLockscreenChecks: Boolean,
         fillInIntent: Intent?,
-        extraOptions: Bundle?
+        extraOptions: Bundle?,
+        customMessage: String?,
     ) {
         TODO("Not yet implemented b/308819693")
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 28117e9..491db30 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -1329,7 +1329,9 @@
                                                 .putExtra(Intent.EXTRA_TEXT, message.toString()),
                                         "Share rejected touch report")
                                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
-                        true /* onlyProvisioned */, true /* dismissShade */);
+                        true /* onlyProvisioned */,
+                        true /* dismissShade */,
+                        null /* customMessage */);
             });
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index c5dcb09..4ce9010 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -31,6 +31,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.policy.SystemBarUtils;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
@@ -105,6 +106,8 @@
     private boolean mIsExpanded;
     private int mStatusBarState;
     private AnimationStateHandler mAnimationStateHandler;
+
+    private Handler mBgHandler;
     private int mHeadsUpInset;
 
     // Used for determining the region for touch interaction
@@ -149,7 +152,8 @@
             UiEventLogger uiEventLogger,
             JavaAdapter javaAdapter,
             ShadeInteractor shadeInteractor,
-            AvalancheController avalancheController) {
+            AvalancheController avalancheController,
+            @Background Handler bgHandler) {
         super(context, logger, handler, globalSettings, systemClock, executor,
                 accessibilityManagerWrapper, uiEventLogger, avalancheController);
         Resources resources = mContext.getResources();
@@ -159,7 +163,7 @@
         mGroupMembershipManager = groupMembershipManager;
         mVisualStabilityProvider = visualStabilityProvider;
         mAvalancheController = avalancheController;
-
+        mBgHandler = bgHandler;
         updateResources();
         configurationController.addCallback(new ConfigurationController.ConfigurationListener() {
             @Override
@@ -401,7 +405,11 @@
             // Waiting HUNs in AvalancheController are still promoted to the HUN section and thus
             // seen in open shade; clear them so we don't show them again when the shade closes and
             // reordering is allowed again.
-            mAvalancheController.logDroppedHuns(mAvalancheController.getWaitingKeys().size());
+            int waitingKeysSize = mAvalancheController.getWaitingKeys().size();
+            mBgHandler.post(() -> {
+                // Do this in the background to avoid missing frames when closing the shade
+                mAvalancheController.logDroppedHuns(waitingKeysSize);
+            });
             mAvalancheController.clearNext();
 
             // In open shade the first HUN is pinned, and visual stability logic prevents us from
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 e96326a..bcb613f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
@@ -232,6 +232,7 @@
         skipLockscreenChecks: Boolean,
         fillInIntent: Intent?,
         extraOptions: Bundle?,
+        customMessage: String?,
     ) {
         val animationController =
             if (associatedView is ExpandableNotificationRow) {
@@ -340,6 +341,7 @@
                     afterKeyguardGone = willLaunchResolverActivity,
                     dismissShade = collapse,
                     willAnimateOnKeyguard = animate,
+                    customMessage = customMessage,
                 )
             }
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
index 0218784..45aee5b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
@@ -29,6 +29,7 @@
 import android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
 import android.view.WindowInsets;
 
+import androidx.annotation.VisibleForTesting;
 import com.android.compose.animation.scene.ObservableTransitionState;
 import com.android.internal.policy.SystemBarUtils;
 import com.android.systemui.Dumpable;
@@ -69,6 +70,7 @@
 
     private boolean mIsStatusBarExpanded = false;
     private boolean mIsIdleOnGone = true;
+    private boolean mIsRemoteUserInteractionOngoing = false;
     private boolean mShouldAdjustInsets = false;
     private View mNotificationShadeWindowView;
     private View mNotificationPanelView;
@@ -133,6 +135,9 @@
             javaAdapter.alwaysCollectFlow(
                     sceneInteractor.get().getTransitionState(),
                     this::onSceneChanged);
+            javaAdapter.alwaysCollectFlow(
+                    sceneInteractor.get().isRemoteUserInteractionOngoing(),
+                    this::onRemoteUserInteractionOngoingChanged);
         } else {
             javaAdapter.alwaysCollectFlow(
                     shadeInteractor.isAnyExpanded(),
@@ -179,6 +184,13 @@
         }
     }
 
+    private void onRemoteUserInteractionOngoingChanged(Boolean ongoing) {
+        if (ongoing != mIsRemoteUserInteractionOngoing) {
+            mIsRemoteUserInteractionOngoing = ongoing;
+            updateTouchableRegion();
+        }
+    }
+
     /**
      * Calculates the touch region needed for heads up notifications, taking into consideration
      * any existing display cutouts (notch)
@@ -276,13 +288,15 @@
      * Helper to let us know when calculating the region is not needed because we know the entire
      * screen needs to be touchable.
      */
-    private boolean shouldMakeEntireScreenTouchable() {
+    @VisibleForTesting
+    boolean shouldMakeEntireScreenTouchable() {
         // The touchable region is always the full area when expanded, whether we're showing the
         // shade or the bouncer. It's also fully touchable when the screen off animation is playing
         // since we don't want stray touches to go through the light reveal scrim to whatever is
         // underneath.
         return mIsStatusBarExpanded
-                || (SceneContainerFlag.isEnabled() && !mIsIdleOnGone)
+                || (SceneContainerFlag.isEnabled()
+                && (!mIsIdleOnGone || mIsRemoteUserInteractionOngoing))
                 || mPrimaryBouncerInteractor.isShowing().getValue()
                 || mAlternateBouncerInteractor.isVisibleState()
                 || mUnlockedScreenOffAnimationController.isAnimationPlaying();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/complication/DreamHomeControlsComplicationTest.java
index ddf69b5..c2173c4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/complication/DreamHomeControlsComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/complication/DreamHomeControlsComplicationTest.java
@@ -132,7 +132,7 @@
     public void complicationAvailability_serviceNotAvailable_noFavorites_doNotAddComplication() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent, mMonitor, false);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor);
         registrant.start();
 
         setHaveFavorites(false);
@@ -145,7 +145,7 @@
     public void complicationAvailability_serviceAvailable_noFavorites_doNotAddComplication() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent, mMonitor, false);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor);
         registrant.start();
 
         setHaveFavorites(false);
@@ -158,7 +158,7 @@
     public void complicationAvailability_serviceAvailable_noFavorites_panel_addComplication() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent, mMonitor, false);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor);
         registrant.start();
 
         setHaveFavorites(false);
@@ -171,7 +171,7 @@
     public void complicationAvailability_serviceNotAvailable_haveFavorites_doNotAddComplication() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent, mMonitor, false);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor);
         registrant.start();
 
         setHaveFavorites(true);
@@ -184,7 +184,7 @@
     public void complicationAvailability_serviceAvailable_haveFavorites_addComplication() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent, mMonitor, false);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor);
         registrant.start();
 
         setHaveFavorites(true);
@@ -197,7 +197,7 @@
     public void complicationAvailability_checkAvailabilityWhenDreamOverlayBecomesActive() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mComplication,
-                        mDreamOverlayStateController, mControlsComponent, mMonitor, false);
+                        mDreamOverlayStateController, mControlsComponent, mMonitor);
         registrant.start();
 
         setServiceAvailable(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
index e2cca38..b58eb49 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -33,13 +34,20 @@
 import android.app.IActivityManager;
 import android.app.admin.DevicePolicyManager;
 import android.app.trust.TrustManager;
+import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
 import android.graphics.Color;
+import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.Flags;
 import android.media.AudioManager;
 import android.os.Handler;
 import android.os.UserManager;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
 import android.service.dreams.IDreamManager;
 import android.testing.TestableLooper;
@@ -88,6 +96,7 @@
 import com.android.systemui.util.settings.SecureSettings;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -102,6 +111,9 @@
 @RunWith(AndroidJUnit4.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class GlobalActionsDialogLiteTest extends SysuiTestCase {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
     private GlobalActionsDialogLite mGlobalActionsDialogLite;
 
     @Mock private GlobalActions.GlobalActionsManager mWindowManagerFuncs;
@@ -141,6 +153,7 @@
     @Mock private DialogTransitionAnimator mDialogTransitionAnimator;
     @Mock private SelectedUserInteractor mSelectedUserInteractor;
     @Mock private OnBackInvokedDispatcher mOnBackInvokedDispatcher;
+    @Mock private BiometricManager mBiometricManager;
     @Captor private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback;
 
     private TestableLooper mTestableLooper;
@@ -157,10 +170,13 @@
         when(mUserContextProvider.getUserContext()).thenReturn(mContext);
         when(mResources.getConfiguration()).thenReturn(
                 getContext().getResources().getConfiguration());
+        when(mBiometricManager.canAuthenticate(anyInt())).thenReturn(
+                BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE);
 
         mGlobalSettings = new FakeGlobalSettings();
         mSecureSettings = new FakeSettings();
         mInteractor = mKosmos.getGlobalActionsInteractor();
+        mContext.addMockSystemService(Context.BIOMETRIC_SERVICE, mBiometricManager);
 
         mGlobalActionsDialogLite = new GlobalActionsDialogLite(mContext,
                 mWindowManagerFuncs,
@@ -551,6 +567,35 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS)
+    public void requestBiometricAuth_whenShutDownShortPressAndMandatoryBiometricsActive() {
+        mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite);
+        ArgumentCaptor<BiometricPrompt.AuthenticationCallback>
+                authenticationCallbackArgumentCaptor = ArgumentCaptor.forClass(
+                        BiometricPrompt.AuthenticationCallback.class);
+
+        when(mBiometricManager.canAuthenticate(
+                BiometricManager.Authenticators.MANDATORY_BIOMETRICS)).thenReturn(
+                        BiometricManager.BIOMETRIC_SUCCESS);
+        doNothing().when(mGlobalActionsDialogLite).launchBiometricPromptForMandatoryBiometrics(
+                any());
+
+        GlobalActionsDialogLite.ShutDownAction shutDownAction =
+                mGlobalActionsDialogLite.new ShutDownAction();
+        shutDownAction.onPress();
+
+        verify(mGlobalActionsDialogLite).launchBiometricPromptForMandatoryBiometrics(
+                authenticationCallbackArgumentCaptor.capture());
+
+        BiometricPrompt.AuthenticationCallback authenticationCallback =
+                authenticationCallbackArgumentCaptor.getValue();
+        authenticationCallback.onAuthenticationSucceeded(null);
+
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_SHUTDOWN_PRESS);
+        verify(mWindowManagerFuncs).shutdown();
+    }
+
+    @Test
     public void testShouldLogLockdownPress() {
         GlobalActionsDialogLite.LockDownAction lockDownAction =
                 mGlobalActionsDialogLite.new LockDownAction();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
new file mode 100644
index 0000000..6985439
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.shortcut.data.repository
+
+import android.view.KeyEvent
+import android.view.KeyboardShortcutGroup
+import android.view.KeyboardShortcutInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
+import com.android.systemui.keyboard.shortcut.shared.model.shortcutCategory
+import com.android.systemui.keyboard.shortcut.shortcutHelperCategoriesRepository
+import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShortcutHelperCategoriesRepositoryTest : SysuiTestCase() {
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private val kosmos = testKosmos().also { it.testDispatcher = UnconfinedTestDispatcher() }
+    private val repo = kosmos.shortcutHelperCategoriesRepository
+    private val helper = kosmos.shortcutHelperTestHelper
+    private val testScope = kosmos.testScope
+
+    @Test
+    fun stateActive_imeShortcuts_shortcutInfoCorrectlyConverted() =
+        testScope.runTest {
+            helper.setImeShortcuts(imeShortcutsGroupWithPreviousLanguageSwitchShortcut)
+            val imeShortcutCategory by collectLastValue(repo.imeShortcutsCategory)
+
+            helper.showFromActivity()
+
+            assertThat(imeShortcutCategory)
+                .isEqualTo(expectedImeShortcutCategoryWithPreviousLanguageSwitchShortcut)
+        }
+
+    @Test
+    fun stateActive_imeShortcuts_discardUnsupportedShortcutInfoModifiers() =
+        testScope.runTest {
+            helper.setImeShortcuts(imeShortcutsGroupWithUnsupportedShortcutModifiers)
+            val imeShortcutCategory by collectLastValue(repo.imeShortcutsCategory)
+
+            helper.showFromActivity()
+
+            assertThat(imeShortcutCategory)
+                .isEqualTo(expectedImeShortcutCategoryWithDiscardedUnsupportedShortcuts)
+        }
+
+    private val switchToPreviousLanguageCommand =
+        ShortcutCommand(
+            listOf(KeyEvent.META_CTRL_ON, KeyEvent.META_SHIFT_ON, KeyEvent.KEYCODE_SPACE)
+        )
+
+    private val expectedImeShortcutCategoryWithDiscardedUnsupportedShortcuts =
+        shortcutCategory(ShortcutCategoryType.IME) { subCategory("input", emptyList()) }
+
+    private val switchToPreviousLanguageKeyboardShortcutInfo =
+        KeyboardShortcutInfo(
+            /* label = */ "switch to previous language",
+            /* keycode = */ switchToPreviousLanguageCommand.keyCodes[2],
+            /* modifiers = */ switchToPreviousLanguageCommand.keyCodes[0] or
+                switchToPreviousLanguageCommand.keyCodes[1],
+        )
+
+    private val expectedImeShortcutCategoryWithPreviousLanguageSwitchShortcut =
+        shortcutCategory(ShortcutCategoryType.IME) {
+            subCategory(
+                "input",
+                listOf(
+                    Shortcut(
+                        switchToPreviousLanguageKeyboardShortcutInfo.label!!.toString(),
+                        listOf(switchToPreviousLanguageCommand)
+                    )
+                )
+            )
+        }
+
+    private val imeShortcutsGroupWithPreviousLanguageSwitchShortcut =
+        listOf(
+            KeyboardShortcutGroup(
+                "input",
+                listOf(
+                    switchToPreviousLanguageKeyboardShortcutInfo,
+                )
+            )
+        )
+
+    private val shortcutInfoWithUnsupportedModifier =
+        KeyboardShortcutInfo(
+            /* label = */ "unsupported shortcut",
+            /* keycode = */ KeyEvent.KEYCODE_SPACE,
+            /* modifiers = */ 32
+        )
+
+    private val imeShortcutsGroupWithUnsupportedShortcutModifiers =
+        listOf(
+            KeyboardShortcutGroup(
+                "input",
+                listOf(
+                    shortcutInfoWithUnsupportedModifier,
+                )
+            )
+        )
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
index 9c9e48e..5c7ce3e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
@@ -16,10 +16,17 @@
 
 package com.android.systemui.keyboard.shortcut.domain.interactor
 
+import android.view.KeyEvent
+import android.view.KeyboardShortcutGroup
+import android.view.KeyboardShortcutInfo
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
+import com.android.systemui.keyboard.shortcut.shared.model.shortcut
 import com.android.systemui.keyboard.shortcut.shortcutHelperCategoriesInteractor
 import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource
@@ -57,6 +64,7 @@
     @Test
     fun categories_stateActive_emitsAllCategoriesInOrder() =
         testScope.runTest {
+            helper.setImeShortcuts(imeShortcutGroups)
             val categories by collectLastValue(interactor.shortcutCategories)
 
             helper.showFromActivity()
@@ -64,7 +72,8 @@
             assertThat(categories)
                 .containsExactly(
                     systemShortcutsSource.systemShortcutsCategory(),
-                    multitaskingShortcutsSource.multitaskingShortcutCategory()
+                    multitaskingShortcutsSource.multitaskingShortcutCategory(),
+                    imeShortcutCategory
                 )
                 .inOrder()
         }
@@ -78,4 +87,165 @@
 
             assertThat(categories).isEmpty()
         }
+
+    fun categories_stateActive_emitsGroupedShortcuts() =
+        testScope.runTest {
+            helper.setImeShortcuts(imeShortcutsGroupsWithDuplicateLabels)
+            val categories by collectLastValue(interactor.shortcutCategories)
+
+            helper.showFromActivity()
+
+            assertThat(categories)
+                .containsExactly(
+                    systemShortcutsSource.systemShortcutsCategory(),
+                    multitaskingShortcutsSource.multitaskingShortcutCategory(),
+                    expectedGroupedShortcutCategories
+                )
+        }
+
+    private val switchToNextLanguageShortcut =
+        shortcut(label = "switch to next language") {
+            command(KeyEvent.META_CTRL_ON, KeyEvent.KEYCODE_SPACE)
+        }
+
+    private val switchToNextLanguageKeyboardShortcutInfo =
+        KeyboardShortcutInfo(
+            /* label = */ switchToNextLanguageShortcut.label,
+            /* keycode = */ switchToNextLanguageShortcut.commands[0].keyCodes[1],
+            /* modifiers = */ switchToNextLanguageShortcut.commands[0].keyCodes[0],
+        )
+
+    private val switchToNextLanguageShortcutAlternative =
+        shortcut("switch to next language") {
+            command(KeyEvent.META_CTRL_ON, KeyEvent.KEYCODE_SPACE)
+        }
+
+    private val switchToNextLanguageKeyboardShortcutInfoAlternative =
+        KeyboardShortcutInfo(
+            /* label = */ switchToNextLanguageShortcutAlternative.label,
+            /* keycode = */ switchToNextLanguageShortcutAlternative.commands[0].keyCodes[1],
+            /* modifiers = */ switchToNextLanguageShortcutAlternative.commands[0].keyCodes[0],
+        )
+
+    private val switchToPreviousLanguageShortcut =
+        shortcut("switch to previous language") {
+            command(
+                KeyEvent.META_SHIFT_ON,
+                KeyEvent.KEYCODE_SPACE,
+            )
+        }
+
+    private val switchToPreviousLanguageKeyboardShortcutInfo =
+        KeyboardShortcutInfo(
+            /* label = */ switchToPreviousLanguageShortcut.label,
+            /* keycode = */ switchToPreviousLanguageShortcut.commands[0].keyCodes[1],
+            /* modifiers = */ switchToPreviousLanguageShortcut.commands[0].keyCodes[0],
+        )
+
+    private val switchToPreviousLanguageShortcutAlternative =
+        shortcut("switch to previous language") {
+            command(
+                KeyEvent.META_SHIFT_ON,
+                KeyEvent.KEYCODE_SPACE,
+            )
+        }
+
+    private val switchToPreviousLanguageKeyboardShortcutInfoAlternative =
+        KeyboardShortcutInfo(
+            /* label = */ switchToPreviousLanguageShortcutAlternative.label,
+            /* keycode = */ switchToPreviousLanguageShortcutAlternative.commands[0].keyCodes[1],
+            /* modifiers = */ switchToPreviousLanguageShortcutAlternative.commands[0].keyCodes[0],
+        )
+
+    private val showOnscreenKeyboardShortcut =
+        shortcut(label = "Show on-screen keyboard") {
+            command(KeyEvent.META_ALT_ON, KeyEvent.KEYCODE_K)
+        }
+
+    private val showOnScreenKeyboardShortcutInfo =
+        KeyboardShortcutInfo(
+            /* label = */ showOnscreenKeyboardShortcut.label,
+            /* keycode = */ showOnscreenKeyboardShortcut.commands[0].keyCodes[1],
+            /* modifiers = */ showOnscreenKeyboardShortcut.commands[0].keyCodes[0],
+        )
+
+    private val accessClipboardShortcut =
+        shortcut(label = "Access clipboard") { command(KeyEvent.META_ALT_ON, KeyEvent.KEYCODE_V) }
+
+    private val accessClipboardShortcutInfo =
+        KeyboardShortcutInfo(
+            /* label = */ accessClipboardShortcut.label,
+            /* keycode = */ accessClipboardShortcut.commands[0].keyCodes[1],
+            /* modifiers = */ accessClipboardShortcut.commands[0].keyCodes[0],
+        )
+
+    private val imeShortcutGroups =
+        listOf(
+            KeyboardShortcutGroup(
+                /* label = */ "input",
+                /* shortcutInfoList = */ listOf(
+                    switchToNextLanguageKeyboardShortcutInfo,
+                    switchToPreviousLanguageKeyboardShortcutInfo
+                )
+            )
+        )
+
+    private val imeShortcutCategory =
+        ShortcutCategory(
+            type = ShortcutCategoryType.IME,
+            subCategories =
+                listOf(
+                    ShortcutSubCategory(
+                        imeShortcutGroups[0].label.toString(),
+                        listOf(switchToNextLanguageShortcut, switchToPreviousLanguageShortcut)
+                    )
+                )
+        )
+
+    private val imeShortcutsGroupsWithDuplicateLabels =
+        listOf(
+            KeyboardShortcutGroup(
+                "input",
+                listOf(
+                    switchToNextLanguageKeyboardShortcutInfo,
+                    switchToNextLanguageKeyboardShortcutInfoAlternative,
+                    switchToPreviousLanguageKeyboardShortcutInfo,
+                    switchToPreviousLanguageKeyboardShortcutInfoAlternative
+                )
+            ),
+            KeyboardShortcutGroup(
+                "Gboard",
+                listOf(
+                    showOnScreenKeyboardShortcutInfo,
+                    accessClipboardShortcutInfo,
+                )
+            )
+        )
+
+    private val expectedGroupedShortcutCategories =
+        ShortcutCategory(
+            type = ShortcutCategoryType.IME,
+            subCategories =
+                listOf(
+                    ShortcutSubCategory(
+                        imeShortcutsGroupsWithDuplicateLabels[0].label.toString(),
+                        listOf(
+                            switchToNextLanguageShortcut.copy(
+                                commands =
+                                    switchToNextLanguageShortcut.commands +
+                                        switchToNextLanguageShortcutAlternative.commands
+                            ),
+                            switchToPreviousLanguageShortcut.copy(
+                                commands =
+                                    switchToPreviousLanguageShortcut.commands +
+                                        switchToPreviousLanguageShortcutAlternative.commands
+                            )
+                        ),
+                    ),
+                    ShortcutSubCategory(
+                        imeShortcutsGroupsWithDuplicateLabels[1].label.toString(),
+                        listOf(showOnscreenKeyboardShortcut, accessClipboardShortcut),
+                    )
+                )
+        )
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
index 90ac05f..506c5ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
@@ -135,7 +135,6 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
-                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
         val remoteUserSelectionManager =
@@ -481,8 +480,7 @@
                         )
                     }
                 }
-            }
-            ?: emptyList()
+            } ?: emptyList()
     }
 
     private fun querySlots(): List<Slot> {
@@ -517,8 +515,7 @@
                         )
                     }
                 }
-            }
-            ?: emptyList()
+            } ?: emptyList()
     }
 
     private fun queryAffordances(): List<Affordance> {
@@ -558,8 +555,7 @@
                         )
                     }
                 }
-            }
-            ?: emptyList()
+            } ?: emptyList()
     }
 
     data class Slot(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
index 27b9863..f726aae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
@@ -7,6 +7,7 @@
 import android.graphics.Rect
 import android.os.PowerManager
 import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.testing.TestableLooper.RunWithLooper
 import android.view.RemoteAnimationTarget
 import android.view.SurfaceControl
@@ -99,6 +100,13 @@
             mock(ActivityManager.RunningTaskInfo::class.java), false)
     private lateinit var wallpaperTargets: Array<RemoteAnimationTarget>
 
+    private var surfaceControlLockWp = mock(SurfaceControl::class.java)
+    private var lockWallpaperTarget = RemoteAnimationTarget(
+            3 /* taskId */, 0, surfaceControlLockWp, false, Rect(), Rect(), 0, Point(), Rect(),
+            Rect(), mock(WindowConfiguration::class.java), false, surfaceControlLockWp,
+            Rect(), mock(ActivityManager.RunningTaskInfo::class.java), false)
+    private lateinit var lockWallpaperTargets: Array<RemoteAnimationTarget>
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -118,6 +126,7 @@
         // appear amount setter doesn't short circuit.
         remoteAnimationTargets = arrayOf(remoteTarget1)
         wallpaperTargets = arrayOf(wallpaperTarget)
+        lockWallpaperTargets = arrayOf(lockWallpaperTarget)
 
         // Set the surface applier to our mock so that we can verify the arguments passed to it.
         // This applier does not have any side effects within the unlock animation controller, so
@@ -144,6 +153,7 @@
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
             remoteAnimationTargets,
             arrayOf(),
+            arrayOf(),
             0 /* startTime */,
             false /* requestedShowSurfaceBehindKeyguard */
         )
@@ -177,6 +187,7 @@
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
             remoteAnimationTargets,
             wallpaperTargets,
+            arrayOf(),
             0 /* startTime */,
             false /* requestedShowSurfaceBehindKeyguard */
         )
@@ -199,6 +210,7 @@
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
             remoteAnimationTargets,
             wallpaperTargets,
+            arrayOf(),
             0 /* startTime */,
             false /* requestedShowSurfaceBehindKeyguard */
         )
@@ -219,6 +231,7 @@
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
             remoteAnimationTargets,
             wallpaperTargets,
+            arrayOf(),
             0 /* startTime */,
             false /* requestedShowSurfaceBehindKeyguard */
         )
@@ -242,6 +255,7 @@
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
             remoteAnimationTargets,
             wallpaperTargets,
+            arrayOf(),
             0 /* startTime */,
             true /* requestedShowSurfaceBehindKeyguard */
         )
@@ -265,6 +279,7 @@
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
             remoteAnimationTargets,
             wallpaperTargets,
+            arrayOf(),
             0 /* startTime */,
             true /* requestedShowSurfaceBehindKeyguard */
         )
@@ -286,6 +301,7 @@
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
             remoteAnimationTargets,
             wallpaperTargets,
+            arrayOf(),
             0 /* startTime */,
             false /* requestedShowSurfaceBehindKeyguard */
         )
@@ -301,6 +317,7 @@
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
             remoteAnimationTargets,
             wallpaperTargets,
+            arrayOf(),
             0 /* startTime */,
             true /* requestedShowSurfaceBehindKeyguard */
         )
@@ -317,6 +334,7 @@
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
                 remoteAnimationTargets,
                 wallpaperTargets,
+                arrayOf(),
                 0 /* startTime */,
                 false /* requestedShowSurfaceBehindKeyguard */
         )
@@ -325,6 +343,53 @@
     }
 
     /**
+     * The canned animation should launch a cross fade when there are different wallpapers on lock
+     * and home screen.
+     */
+    @Test
+    @EnableFlags(Flags.FLAG_FASTER_UNLOCK_TRANSITION)
+    fun manualUnlock_multipleWallpapers() {
+        var lastFadeInAlpha = -1f
+        var lastFadeOutAlpha = -1f
+
+        keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
+                arrayOf(remoteTarget1, remoteTarget2),
+                wallpaperTargets,
+                lockWallpaperTargets,
+                0 /* startTime */,
+                false /* requestedShowSurfaceBehindKeyguard */
+        )
+
+        for (i in 0..10) {
+            clearInvocations(surfaceTransactionApplier)
+            val amount = i / 10f
+
+            keyguardUnlockAnimationController.setSurfaceBehindAppearAmount(amount)
+
+            val captorSb = ArgThatCaptor<SyncRtSurfaceTransactionApplier.SurfaceParams>()
+            verify(surfaceTransactionApplier, times(2)).scheduleApply(
+                    captorSb.capture { sp ->
+                        sp.surface == surfaceControlWp || sp.surface == surfaceControlLockWp })
+
+            val fadeInAlpha = captorSb.getLastValue { it.surface == surfaceControlWp }.alpha
+            val fadeOutAlpha = captorSb.getLastValue { it.surface == surfaceControlLockWp }.alpha
+
+            if (amount == 0f) {
+                assertTrue (fadeInAlpha == 0f)
+                assertTrue (fadeOutAlpha == 1f)
+            } else if (amount == 1f) {
+                assertTrue (fadeInAlpha == 1f)
+                assertTrue (fadeOutAlpha == 0f)
+            } else {
+                assertTrue(fadeInAlpha >= lastFadeInAlpha)
+                assertTrue(fadeOutAlpha <= lastFadeOutAlpha)
+            }
+            lastFadeInAlpha = fadeInAlpha
+            lastFadeOutAlpha = fadeOutAlpha
+        }
+    }
+
+    /**
      * If we are not wake and unlocking, we expect the unlock animation to play normally.
      */
     @Test
@@ -333,6 +398,7 @@
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
                 arrayOf(remoteTarget1, remoteTarget2),
                 wallpaperTargets,
+                arrayOf(),
                 0 /* startTime */,
                 false /* requestedShowSurfaceBehindKeyguard */
         )
@@ -378,6 +444,7 @@
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
                 remoteAnimationTargets,
                 wallpaperTargets,
+                arrayOf(),
                 0 /* startTime */,
                 false /* requestedShowSurfaceBehindKeyguard */
         )
@@ -387,7 +454,7 @@
         clearInvocations(surfaceTransactionApplier)
 
         keyguardUnlockAnimationController.setSurfaceBehindAppearAmount(1f)
-        keyguardUnlockAnimationController.setWallpaperAppearAmount(1f)
+        keyguardUnlockAnimationController.setWallpaperAppearAmount(1f, wallpaperTargets)
 
         val captorSb = ArgThatCaptor<SyncRtSurfaceTransactionApplier.SurfaceParams>()
         verify(surfaceTransactionApplier, times(1)).scheduleApply(
@@ -414,6 +481,7 @@
         keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
                 remoteAnimationTargets,
                 wallpaperTargets,
+                arrayOf(),
                 0 /* startTime */,
                 false /* requestedShowSurfaceBehindKeyguard */
         )
@@ -423,7 +491,7 @@
         clearInvocations(surfaceTransactionApplier)
 
         keyguardUnlockAnimationController.setSurfaceBehindAppearAmount(1f)
-        keyguardUnlockAnimationController.setWallpaperAppearAmount(1f)
+        keyguardUnlockAnimationController.setWallpaperAppearAmount(1f, wallpaperTargets)
 
         val captorSb = ArgThatCaptor<SyncRtSurfaceTransactionApplier.SurfaceParams>()
         verify(surfaceTransactionApplier, times(1)).scheduleApply(
@@ -532,8 +600,8 @@
             }
         }
 
-        fun getLastValue(): T {
-            return allArgs.last()
+        fun getLastValue(predicate: Predicate<T>? = null): T {
+            return if (predicate != null) allArgs.last(predicate::test) else allArgs.last()
         }
 
         fun getAllValues(): List<T> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index d2a9c58..7560a97 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -63,9 +63,6 @@
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4
-import platform.test.runner.parameterized.Parameters
-import platform.test.runner.parameterized.Parameter
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyInt
@@ -76,6 +73,9 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyZeroInteractions
 import org.mockito.MockitoAnnotations
+import platform.test.runner.parameterized.Parameter
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @FlakyTest(
@@ -281,7 +281,6 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
-                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
         val remoteUserSelectionManager =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
index 9d06031..fd1bf54 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
@@ -63,9 +63,6 @@
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4
-import platform.test.runner.parameterized.Parameters
-import platform.test.runner.parameterized.Parameter
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyInt
@@ -76,6 +73,9 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyZeroInteractions
 import org.mockito.MockitoAnnotations
+import platform.test.runner.parameterized.Parameter
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @FlakyTest(
@@ -281,7 +281,6 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
-                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
         val remoteUserSelectionManager =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index 49a72e2..4f4aac4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -179,7 +179,6 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
-                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
         val remoteUserSelectionManager =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
index e33d75c..9fb1aa7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
@@ -221,7 +221,6 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
-                systemSettings = FakeSettings(),
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
         val remoteUserSelectionManager =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
index a770722..fbfe41f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
@@ -1799,6 +1799,7 @@
                 any(),
                 eq(null),
                 eq(null),
+                eq(null),
             )
         verify(activityStarter, never()).postStartActivityDismissingKeyguard(eq(clickIntent), any())
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
index 6e6e311..e1c3911 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
@@ -23,6 +23,7 @@
 import android.os.PowerManager
 import android.os.Process
 import android.os.UserHandle
+import android.os.UserManager
 import android.testing.TestableContext
 import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -108,6 +109,7 @@
     @Mock private lateinit var navModeController: NavigationModeController
     @Mock private lateinit var statusBarWinController: NotificationShadeWindowController
     @Mock private lateinit var userTracker: UserTracker
+    @Mock private lateinit var userManager: UserManager
     @Mock private lateinit var uiEventLogger: UiEventLogger
     @Mock private lateinit var sysuiUnlockAnimationController: KeyguardUnlockAnimationController
     @Mock
@@ -199,11 +201,12 @@
     }
 
     @Test
-    fun connectToOverviewService_primaryUser_expectBindService() {
+    fun connectToOverviewService_primaryUserNoVisibleBgUsersSupported_expectBindService() {
         val mockitoSession =
             ExtendedMockito.mockitoSession().spyStatic(Process::class.java).startMocking()
         try {
             `when`(Process.myUserHandle()).thenReturn(UserHandle.SYSTEM)
+            `when`(userManager.isVisibleBackgroundUsersSupported()).thenReturn(false)
             val spyContext = spy(context)
             val ops = createOverviewProxyService(spyContext)
             ops.startConnectionToCurrentUser()
@@ -214,11 +217,46 @@
     }
 
     @Test
-    fun connectToOverviewService_nonPrimaryUser_expectNoBindService() {
+    fun connectToOverviewService_nonPrimaryUserNoVisibleBgUsersSupported_expectNoBindService() {
         val mockitoSession =
             ExtendedMockito.mockitoSession().spyStatic(Process::class.java).startMocking()
         try {
             `when`(Process.myUserHandle()).thenReturn(UserHandle.of(12345))
+            `when`(userManager.isVisibleBackgroundUsersSupported()).thenReturn(false)
+            val spyContext = spy(context)
+            val ops = createOverviewProxyService(spyContext)
+            ops.startConnectionToCurrentUser()
+            verify(spyContext, times(0)).bindServiceAsUser(any(), any(), anyInt(), any())
+        } finally {
+            mockitoSession.finishMocking()
+        }
+    }
+
+    @Test
+    fun connectToOverviewService_nonPrimaryBgUserVisibleBgUsersSupported_expectBindService() {
+        val mockitoSession =
+            ExtendedMockito.mockitoSession().spyStatic(Process::class.java).startMocking()
+        try {
+            `when`(Process.myUserHandle()).thenReturn(UserHandle.of(12345))
+            `when`(userManager.isVisibleBackgroundUsersSupported()).thenReturn(true)
+            `when`(userManager.isUserForeground()).thenReturn(false)
+            val spyContext = spy(context)
+            val ops = createOverviewProxyService(spyContext)
+            ops.startConnectionToCurrentUser()
+            verify(spyContext, atLeast(1)).bindServiceAsUser(any(), any(), anyInt(), any())
+        } finally {
+            mockitoSession.finishMocking()
+        }
+    }
+
+    @Test
+    fun connectToOverviewService_nonPrimaryFgUserVisibleBgUsersSupported_expectNoBindService() {
+        val mockitoSession =
+            ExtendedMockito.mockitoSession().spyStatic(Process::class.java).startMocking()
+        try {
+            `when`(Process.myUserHandle()).thenReturn(UserHandle.of(12345))
+            `when`(userManager.isVisibleBackgroundUsersSupported()).thenReturn(true)
+            `when`(userManager.isUserForeground()).thenReturn(true)
             val spyContext = spy(context)
             val ops = createOverviewProxyService(spyContext)
             ops.startConnectionToCurrentUser()
@@ -242,6 +280,7 @@
             sysUiState,
             mock(),
             userTracker,
+            userManager,
             wakefulnessLifecycle,
             uiEventLogger,
             displayTracker,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java
index 6733ead..809fb3f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java
@@ -26,6 +26,7 @@
 
 import static com.android.internal.infra.AndroidFuture.completedFuture;
 import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED;
+import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_CLIP_DATA;
 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -37,6 +38,7 @@
 import static org.mockito.Mockito.when;
 
 import android.app.Activity;
+import android.content.ClipData;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -81,6 +83,7 @@
 
     private static final String TEST_URI_STRING = "www.test-uri.com";
     private static final Uri TEST_URI = Uri.parse(TEST_URI_STRING);
+    private static final ClipData TEST_CLIP_DATA = ClipData.newRawUri("Test backlinks", TEST_URI);
     private static final int TEST_UID = 42;
     private static final String TEST_CALLING_PACKAGE = "test-calling-package";
 
@@ -238,6 +241,7 @@
         Bundle bundle = new Bundle();
         bundle.putParcelable(EXTRA_SCREENSHOT_URI, TEST_URI);
         bundle.putInt(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
+        bundle.putParcelable(EXTRA_CLIP_DATA, TEST_CLIP_DATA);
         activity.getResultReceiverForTest().send(Activity.RESULT_OK, bundle);
         waitForIdleSync();
 
@@ -245,7 +249,10 @@
         assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
         assertThat(getStatusCodeExtra(actualResult.getResultData()))
                 .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
-        assertThat(actualResult.getResultData().getData()).isEqualTo(TEST_URI);
+
+        Intent resultData = actualResult.getResultData();
+        assertThat(resultData.getData()).isEqualTo(TEST_URI);
+        assertThat(resultData.getClipData()).isEqualTo(TEST_CLIP_DATA);
         assertThat(mActivityRule.getActivity().isFinishing()).isTrue();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
index e984200..a7f36c3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
@@ -20,6 +20,7 @@
 import android.app.Notification.CATEGORY_EVENT
 import android.app.Notification.CATEGORY_REMINDER
 import android.app.NotificationManager
+import android.content.pm.PackageManager.PERMISSION_DENIED
 import android.content.pm.PackageManager.PERMISSION_GRANTED
 import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -28,11 +29,16 @@
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE
-import java.util.Optional
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mockito.anyString
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
+import org.mockito.kotlin.whenever
+import java.util.Optional
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -58,7 +64,9 @@
             avalancheProvider,
             systemSettings,
             packageManager,
-            Optional.of(bubbles)
+            Optional.of(bubbles),
+            context,
+            notificationManager
         )
     }
 
@@ -87,12 +95,60 @@
     // because avalanche code is based on the suppression refactor.
 
     @Test
+    fun testAvalancheFilter_suppress_hasNotSeenEdu_showEduHun() {
+        setAllowedEmergencyPkg(false)
+        whenever(avalancheProvider.timeoutMs).thenReturn(20)
+        whenever(avalancheProvider.startTime).thenReturn(whenAgo(10))
+
+        val avalancheSuppressor = AvalancheSuppressor(
+            avalancheProvider, systemClock, systemSettings, packageManager,
+            uiEventLogger, context, notificationManager
+        )
+        avalancheSuppressor.hasSeenEdu = false
+
+        withFilter(avalancheSuppressor) {
+            ensurePeekState()
+            assertShouldNotHeadsUp(
+                buildEntry {
+                    importance = NotificationManager.IMPORTANCE_HIGH
+                    whenMs = whenAgo(5)
+                }
+            )
+        }
+        verify(notificationManager, times(1)).notify(anyInt(), any())
+    }
+
+    @Test
+    fun testAvalancheFilter_suppress_hasSeenEduHun_doNotShowEduHun() {
+        setAllowedEmergencyPkg(false)
+        whenever(avalancheProvider.timeoutMs).thenReturn(20)
+        whenever(avalancheProvider.startTime).thenReturn(whenAgo(10))
+
+        val avalancheSuppressor = AvalancheSuppressor(
+            avalancheProvider, systemClock, systemSettings, packageManager,
+            uiEventLogger, context, notificationManager
+        )
+        avalancheSuppressor.hasSeenEdu = true
+
+        withFilter(avalancheSuppressor) {
+            ensurePeekState()
+            assertShouldNotHeadsUp(
+                buildEntry {
+                    importance = NotificationManager.IMPORTANCE_HIGH
+                    whenMs = whenAgo(5)
+                }
+            )
+        }
+        verify(notificationManager, times(0)).notify(anyInt(), any())
+    }
+
+    @Test
     fun testAvalancheFilter_duringAvalanche_allowConversationFromAfterEvent() {
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -112,7 +168,7 @@
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
             assertShouldNotHeadsUp(
@@ -132,7 +188,7 @@
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -150,7 +206,7 @@
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -168,7 +224,7 @@
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -186,7 +242,7 @@
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -204,7 +260,7 @@
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             assertFsiNotSuppressed()
         }
@@ -216,7 +272,7 @@
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -228,20 +284,24 @@
         }
     }
 
-    @Test
-    fun testAvalancheFilter_duringAvalanche_allowEmergency() {
-        avalancheProvider.startTime = whenAgo(10)
-
+    private fun setAllowedEmergencyPkg(allow: Boolean) {
         `when`(
             packageManager.checkPermission(
                 org.mockito.Mockito.eq(permission.RECEIVE_EMERGENCY_BROADCAST),
                 anyString()
             )
-        ).thenReturn(PERMISSION_GRANTED)
+        ).thenReturn(if (allow) PERMISSION_GRANTED else PERMISSION_DENIED)
+    }
+
+    @Test
+    fun testAvalancheFilter_duringAvalanche_allowEmergency() {
+        avalancheProvider.startTime = whenAgo(10)
+
+        setAllowedEmergencyPkg(true)
 
         withFilter(
             AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
-                    uiEventLogger)
+                    uiEventLogger, context, notificationManager)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
index a457405..378705a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
@@ -31,6 +31,7 @@
 import android.app.Notification.GROUP_ALERT_SUMMARY
 import android.app.Notification.VISIBILITY_PRIVATE
 import android.app.NotificationChannel
+import android.app.NotificationManager
 import android.app.NotificationManager.IMPORTANCE_DEFAULT
 import android.app.NotificationManager.IMPORTANCE_HIGH
 import android.app.NotificationManager.IMPORTANCE_LOW
@@ -133,7 +134,7 @@
     protected val bubbles: Bubbles = mock()
     lateinit var systemSettings: SystemSettings
     protected val packageManager: PackageManager = mock()
-
+    protected val notificationManager: NotificationManager = mock()
     protected abstract val provider: VisualInterruptionDecisionProvider
 
     private val neverSuppresses = object : NotificationInterruptSuppressor {}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt
index 01e638b..f4cebd7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.statusbar.notification.interruption
 
+import android.app.NotificationManager
+import android.content.Context
 import android.content.pm.PackageManager
 import android.hardware.display.AmbientDisplayConfiguration
 import android.os.Handler
@@ -58,6 +60,8 @@
         systemSettings: SystemSettings,
         packageManager: PackageManager,
         bubbles: Optional<Bubbles>,
+        context: Context,
+        notificationManager: NotificationManager
     ): VisualInterruptionDecisionProvider {
         return if (VisualInterruptionRefactor.isEnabled) {
             VisualInterruptionDecisionProviderImpl(
@@ -79,7 +83,9 @@
                 avalancheProvider,
                 systemSettings,
                 packageManager,
-                bubbles
+                bubbles,
+                context,
+                notificationManager
             )
         } else {
             NotificationInterruptStateProviderWrapper(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 164a06e..e8349b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -862,11 +862,12 @@
 
     @Test
     @EnableFlags(ExpandHeadsUpOnInlineReply.FLAG_NAME)
-    public void isExpanded_systemExpandedTrueForHeadsUp_notExpanded() throws Exception {
+    public void isExpanded_HUNsystemExpandedTrueForPinned_notExpanded() throws Exception {
         // GIVEN
         final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
         row.setOnKeyguard(false);
         row.setSystemExpanded(true);
+        row.setPinned(true);
         row.setHeadsUp(true);
 
         // THEN
@@ -875,12 +876,27 @@
 
     @Test
     @EnableFlags(ExpandHeadsUpOnInlineReply.FLAG_NAME)
-    public void isExpanded_systemExpandedTrueForHeadsUpDisappearRunning_notExpanded()
+    public void isExpanded_HUNsystemExpandedTrueForNotPinned_expanded() throws Exception {
+        // GIVEN
+        final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        row.setOnKeyguard(false);
+        row.setSystemExpanded(true);
+        row.setPinned(false);
+        row.setHeadsUp(true);
+
+        // THEN
+        assertThat(row.isExpanded()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(ExpandHeadsUpOnInlineReply.FLAG_NAME)
+    public void isExpanded_HUNDisappearingsystemExpandedTrueForPinned_notExpanded()
             throws Exception {
         // GIVEN
         final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
         row.setOnKeyguard(false);
         row.setSystemExpanded(true);
+        row.setPinned(true);
         row.setHeadsUpAnimatingAway(true);
 
         // THEN
@@ -889,6 +905,21 @@
 
     @Test
     @EnableFlags(ExpandHeadsUpOnInlineReply.FLAG_NAME)
+    public void isExpanded_HUNDisappearingsystemExpandedTrueForNotPinned_expanded()
+            throws Exception {
+        // GIVEN
+        final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+        row.setOnKeyguard(false);
+        row.setSystemExpanded(true);
+        row.setPinned(false);
+        row.setHeadsUpAnimatingAway(true);
+
+        // THEN
+        assertThat(row.isExpanded()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(ExpandHeadsUpOnInlineReply.FLAG_NAME)
     public void isExpanded_userExpandedTrueForHeadsUp_expanded() throws Exception {
         // GIVEN
         final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 1eb33ce..d2540a6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -53,6 +53,7 @@
 
 import android.app.ActivityManager;
 import android.app.IWallpaperManager;
+import android.app.NotificationManager;
 import android.app.WallpaperManager;
 import android.app.trust.TrustManager;
 import android.content.BroadcastReceiver;
@@ -339,6 +340,7 @@
     @Mock private KeyboardShortcuts mKeyboardShortcuts;
     @Mock private KeyboardShortcutListSearch mKeyboardShortcutListSearch;
     @Mock private PackageManager mPackageManager;
+    @Mock private NotificationManager mNotificationManager;
     @Mock private GlanceableHubContainerController mGlanceableHubContainerController;
     @Mock private EmergencyGestureIntentFactory mEmergencyGestureIntentFactory;
 
@@ -399,7 +401,9 @@
                         mAvalancheProvider,
                         mSystemSettings,
                         mPackageManager,
-                        Optional.of(mBubbles));
+                        Optional.of(mBubbles),
+                        mContext,
+                        mNotificationManager);
         mVisualInterruptionDecisionProvider.start();
 
         mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerTest.kt
new file mode 100644
index 0000000..230ddf9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerTest.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.data.repository.sceneContainerRepository
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.testKosmos
+import com.android.systemui.util.kotlin.getValue
+import com.google.common.truth.Truth.assertThat
+import dagger.Lazy
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class StatusBarTouchableRegionManagerTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val sceneRepository = kosmos.sceneContainerRepository
+
+    private val underTest by Lazy { kosmos.statusBarTouchableRegionManager }
+
+    @Test
+    @EnableSceneContainer
+    fun entireScreenTouchable_sceneContainerEnabled_isRemoteUserInteractionOngoing() =
+        testScope.runTest {
+            sceneRepository.setTransitionState(
+                flowOf(ObservableTransitionState.Idle(currentScene = Scenes.Gone))
+            )
+            runCurrent()
+            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()
+
+            sceneRepository.isRemoteUserInteractionOngoing.value = true
+            runCurrent()
+            assertThat(underTest.shouldMakeEntireScreenTouchable()).isTrue()
+
+            sceneRepository.isRemoteUserInteractionOngoing.value = false
+            runCurrent()
+            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()
+        }
+
+    @Test
+    @DisableSceneContainer
+    fun entireScreenTouchable_sceneContainerDisabled_isRemoteUserInteractionOngoing() =
+        testScope.runTest {
+            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()
+
+            sceneRepository.isRemoteUserInteractionOngoing.value = true
+            runCurrent()
+
+            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()
+        }
+
+    @Test
+    @EnableSceneContainer
+    fun entireScreenTouchable_sceneContainerEnabled_isIdleOnGone() =
+        testScope.runTest {
+            sceneRepository.setTransitionState(
+                flowOf(ObservableTransitionState.Idle(currentScene = Scenes.Gone))
+            )
+            runCurrent()
+            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()
+
+            sceneRepository.setTransitionState(
+                flowOf(ObservableTransitionState.Idle(currentScene = Scenes.Shade))
+            )
+            runCurrent()
+            assertThat(underTest.shouldMakeEntireScreenTouchable()).isTrue()
+
+            sceneRepository.setTransitionState(
+                flowOf(ObservableTransitionState.Idle(currentScene = Scenes.Gone))
+            )
+            runCurrent()
+            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()
+        }
+
+    @Test
+    @DisableSceneContainer
+    fun entireScreenTouchable_sceneContainerDisabled_isIdleOnGone() =
+        testScope.runTest {
+            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()
+
+            sceneRepository.setTransitionState(
+                flowOf(ObservableTransitionState.Idle(currentScene = Scenes.Shade))
+            )
+            runCurrent()
+
+            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index c5fbc39..dc7a2c3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -59,6 +59,7 @@
 import android.app.IActivityManager;
 import android.app.INotificationManager;
 import android.app.Notification;
+import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -473,7 +474,9 @@
                         mock(AvalancheProvider.class),
                         mock(SystemSettings.class),
                         mock(PackageManager.class),
-                        Optional.of(mock(Bubbles.class))
+                        Optional.of(mock(Bubbles.class)),
+                        mContext,
+                        mock(NotificationManager.class)
                         );
         interruptionDecisionProvider.start();
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
index f51036f..e00f980 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -19,6 +19,7 @@
 import android.content.applicationContext
 import android.content.res.mainResources
 import android.hardware.input.fakeInputManager
+import android.view.windowManager
 import com.android.systemui.broadcast.broadcastDispatcher
 import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperCategoriesRepository
 import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository
@@ -59,6 +60,8 @@
         ShortcutHelperCategoriesRepository(
             shortcutHelperSystemShortcutsSource,
             shortcutHelperMultiTaskingShortcutsSource,
+            windowManager,
+            shortcutHelperStateRepository
         )
     }
 
@@ -68,7 +71,8 @@
             shortcutHelperStateRepository,
             applicationContext,
             broadcastDispatcher,
-            fakeCommandQueue
+            fakeCommandQueue,
+            windowManager
         )
     }
 
@@ -83,12 +87,7 @@
     }
 
 val Kosmos.shortcutHelperCategoriesInteractor by
-    Kosmos.Fixture {
-        ShortcutHelperCategoriesInteractor(
-            shortcutHelperStateRepository,
-            shortcutHelperCategoriesRepository
-        )
-    }
+    Kosmos.Fixture { ShortcutHelperCategoriesInteractor(shortcutHelperCategoriesRepository) }
 
 val Kosmos.shortcutHelperViewModel by
     Kosmos.Fixture { ShortcutHelperViewModel(testDispatcher, shortcutHelperStateInteractor) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt
index 36608ff..40510db 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt
@@ -18,20 +18,45 @@
 
 import android.content.Context
 import android.content.Intent
+import android.view.KeyboardShortcutGroup
+import android.view.WindowManager
+import android.view.WindowManager.KeyboardShortcutsReceiver
 import com.android.systemui.broadcast.FakeBroadcastDispatcher
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
 
 class ShortcutHelperTestHelper(
     repo: ShortcutHelperStateRepository,
     private val context: Context,
     private val fakeBroadcastDispatcher: FakeBroadcastDispatcher,
     private val fakeCommandQueue: FakeCommandQueue,
+    windowManager: WindowManager
 ) {
 
+    companion object {
+        const val DEFAULT_DEVICE_ID = 123
+    }
+
+    private var imeShortcuts: List<KeyboardShortcutGroup> = emptyList()
+
     init {
+        whenever(windowManager.requestImeKeyboardShortcuts(any(), any())).thenAnswer {
+            val keyboardShortcutReceiver = it.getArgument<KeyboardShortcutsReceiver>(0)
+            keyboardShortcutReceiver.onKeyboardShortcutsReceived(imeShortcuts)
+            return@thenAnswer Unit
+        }
         repo.start()
     }
 
+    /**
+     * Use this method to set what ime shortcuts should be returned from windowManager in tests. By
+     * default windowManager.requestImeKeyboardShortcuts will return emptyList. See init block.
+     */
+    fun setImeShortcuts(imeShortcuts: List<KeyboardShortcutGroup>) {
+        this.imeShortcuts = imeShortcuts
+    }
+
     fun hideThroughCloseSystemDialogs() {
         fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
             context,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/startable/ShadeStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/startable/ShadeStartableKosmos.kt
index b85858d..79b80bc 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/startable/ShadeStartableKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/startable/ShadeStartableKosmos.kt
@@ -27,7 +27,9 @@
 import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.shade.domain.interactor.panelExpansionInteractor
 import com.android.systemui.shade.transition.ScrimShadeTransitionController
+import com.android.systemui.statusbar.notification.stack.notificationStackScrollLayoutController
 import com.android.systemui.statusbar.policy.splitShadeStateController
+import com.android.systemui.statusbar.pulseExpansionHandler
 import com.android.systemui.util.mockito.mock
 
 @Deprecated("ShadeExpansionStateManager is deprecated. Remove your dependency on it instead.")
@@ -45,5 +47,7 @@
         sceneInteractorProvider = { sceneInteractor },
         panelExpansionInteractorProvider = { panelExpansionInteractor },
         shadeExpansionStateManager = shadeExpansionStateManager,
+        pulseExpansionHandler = pulseExpansionHandler,
+        nsslc = notificationStackScrollLayoutController,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerKosmos.kt
new file mode 100644
index 0000000..8785256
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerKosmos.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.systemui.statusbar.phone
+
+import android.content.applicationContext
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.notificationShadeWindowController
+import com.android.systemui.statusbar.policy.configurationController
+import com.android.systemui.statusbar.policy.headsUpManager
+import com.android.systemui.util.kotlin.JavaAdapter
+import com.android.systemui.util.mockito.mock
+import org.mockito.Mockito.mock
+
+var Kosmos.statusBarTouchableRegionManager by
+    Kosmos.Fixture {
+        StatusBarTouchableRegionManager(
+            applicationContext,
+            notificationShadeWindowController,
+            configurationController,
+            headsUpManager,
+            shadeInteractor,
+            { sceneInteractor },
+            JavaAdapter(testScope.backgroundScope),
+            mock<UnlockedScreenOffAnimationController>(),
+            primaryBouncerInteractor,
+            alternateBouncerInteractor,
+        )
+    }
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt
index fec6ff1..0458f53 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/RotationChangeProvider.kt
@@ -28,6 +28,9 @@
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import java.util.concurrent.CopyOnWriteArrayList
+import java.util.concurrent.atomic.AtomicInteger
+
+private const val INVALID_ROTATION = -1
 
 /**
  * Allows to subscribe to rotation changes. Updates are provided for the display associated to
@@ -45,7 +48,7 @@
     private val listeners = CopyOnWriteArrayList<RotationListener>()
 
     private val displayListener = RotationDisplayListener()
-    private var lastRotation: Int? = null
+    private val lastRotation = AtomicInteger(INVALID_ROTATION)
 
     override fun addCallback(listener: RotationListener) {
         bgHandler.post {
@@ -61,7 +64,7 @@
             listeners -= listener
             if (listeners.isEmpty()) {
                 unsubscribeToRotation()
-                lastRotation = null
+                lastRotation.set(INVALID_ROTATION)
             }
         }
     }
@@ -100,9 +103,8 @@
 
                 if (displayId == display.displayId) {
                     val currentRotation = display.rotation
-                    if (lastRotation == null || lastRotation != currentRotation) {
+                    if (lastRotation.compareAndSet(lastRotation.get(), currentRotation)) {
                         listeners.forEach { it.onRotationChanged(currentRotation) }
-                        lastRotation = currentRotation
                     }
                 }
             } finally {
diff --git a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
index f3172ae..bdc3577 100644
--- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
@@ -274,7 +274,9 @@
 android.telephony.ModemActivityInfo
 android.telephony.ServiceState
 
+android.os.connectivity.CellularBatteryStats
 android.os.connectivity.WifiActivityEnergyInfo
+android.os.connectivity.WifiBatteryStats
 
 com.android.server.LocalServices
 
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index f9196f3..d3efa21 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -610,7 +610,7 @@
         }
     }
 
-    void notifyAccessibilityButtonClicked(int displayId) {
+    void notifyMagnificationShortcutTriggered(int displayId) {
         if (mMagnificationGestureHandler.size() != 0) {
             final MagnificationGestureHandler handler = mMagnificationGestureHandler.get(displayId);
             if (handler != null) {
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 1dc3fb4..36d97f6 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -2220,10 +2220,10 @@
         }
     }
 
-    private void sendAccessibilityButtonToInputFilter(int displayId) {
+    private void notifyMagnificationShortcutTriggered(int displayId) {
         synchronized (mLock) {
             if (mHasInputFilter && mInputFilter != null) {
-                mInputFilter.notifyAccessibilityButtonClicked(displayId);
+                mInputFilter.notifyMagnificationShortcutTriggered(displayId);
             }
         }
     }
@@ -3898,7 +3898,7 @@
                             .isActivated(displayId);
             logAccessibilityShortcutActivated(mContext, MAGNIFICATION_COMPONENT_NAME, shortcutType,
                     enabled);
-            sendAccessibilityButtonToInputFilter(displayId);
+            notifyMagnificationShortcutTriggered(displayId);
             return;
         }
         final ComponentName targetComponentName = ComponentName.unflattenFromString(targetName);
diff --git a/services/autofill/java/com/android/server/autofill/RequestId.java b/services/autofill/java/com/android/server/autofill/RequestId.java
index 29ad786..d8069a8 100644
--- a/services/autofill/java/com/android/server/autofill/RequestId.java
+++ b/services/autofill/java/com/android/server/autofill/RequestId.java
@@ -16,8 +16,14 @@
 
 package com.android.server.autofill;
 
-import java.util.List;
+import static com.android.server.autofill.Helper.sDebug;
+
+import android.util.Slog;
+import android.util.SparseArray;
+
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.List;
+import java.util.Random;
 
 // Helper class containing various methods to deal with FillRequest Ids.
 // For authentication flows, there needs to be a way to know whether to retrieve the Fill
@@ -25,56 +31,97 @@
 // way to achieve this is by assigning odd number request ids to secondary provider and
 // even numbers to primary provider.
 public class RequestId {
+    private AtomicInteger sIdCounter;
 
-  private AtomicInteger sIdCounter;
+    // The minimum request id is 2 to avoid possible authentication issues.
+    static final int MIN_REQUEST_ID = 2;
+    // The maximum request id is 0x7FFF to make sure the 16th bit is 0.
+    // This is to make sure the authentication id is always positive.
+    static final int MAX_REQUEST_ID = 0x7FFF; // 32767
 
-  // Mainly used for tests
-  RequestId(int start) {
-    sIdCounter = new AtomicInteger(start);
-  }
+    // The maximum start id is made small to best avoid wrapping around.
+    static final int MAX_START_ID = 1000;
+    // The magic number is used to determine if a wrap has happened.
+    // The underlying assumption of MAGIC_NUMBER is that there can't be as many as MAGIC_NUMBER
+    // of fill requests in one session. so there can't be as many as MAGIC_NUMBER of fill requests
+    // getting dropped.
+    static final int MAGIC_NUMBER = 5000;
 
-  public RequestId() {
-    this((int) (Math.floor(Math.random() * 0xFFFF)));
-  }
+    static final int MIN_PRIMARY_REQUEST_ID = 2;
+    static final int MAX_PRIMARY_REQUEST_ID = 0x7FFE; // 32766
 
-  public static int getLastRequestIdIndex(List<Integer> requestIds) {
-    int lastId = -1;
-    int indexOfBiggest = -1;
-    // Biggest number is usually the latest request, since IDs only increase
-    // The only exception is when the request ID wraps around back to 0
-      for (int i = requestIds.size() - 1; i >= 0; i--) {
-        if (requestIds.get(i) > lastId) {
-        lastId = requestIds.get(i);
-        indexOfBiggest = i;
-      }
-    }
+    static final int MIN_SECONDARY_REQUEST_ID = 3;
+    static final int MAX_SECONDARY_REQUEST_ID = 0x7FFF; // 32767
 
-    // 0xFFFE + 2 == 0x1 (for secondary)
-    // 0xFFFD + 2 == 0x0 (for primary)
-    // Wrap has occurred
-    if (lastId >= 0xFFFD) {
-      // Calculate the biggest size possible
-      // If list only has one kind of request ids - we need to multiple by 2
-      // (since they skip odd ints)
-      // Also subtract one from size because at least one integer exists pre-wrap
-      int calcSize = (requestIds.size()) * 2;
-      //Biggest possible id after wrapping
-      int biggestPossible = (lastId + calcSize) % 0xFFFF;
-      lastId = -1;
-      indexOfBiggest = -1;
-      for (int i = 0; i < requestIds.size(); i++) {
-        int currentId = requestIds.get(i);
-        if (currentId <= biggestPossible && currentId > lastId) {
-          lastId = currentId;
-          indexOfBiggest = i;
+    private static final String TAG = "RequestId";
+
+    // WARNING: This constructor should only be used for testing
+    RequestId(int startId) {
+        if (startId < MIN_REQUEST_ID || startId > MAX_REQUEST_ID) {
+            throw new IllegalArgumentException("startId must be between " + MIN_REQUEST_ID +
+                                                   " and " + MAX_REQUEST_ID);
         }
-      }
+        if (sDebug) {
+            Slog.d(TAG, "RequestId(int): startId= " + startId);
+        }
+        sIdCounter = new AtomicInteger(startId);
     }
 
-    return indexOfBiggest;
-  }
+    // WARNING: This get method should only be used for testing
+    int getRequestId() {
+        return sIdCounter.get();
+    }
 
-  public int nextId(boolean isSecondary) {
+    public RequestId() {
+        Random random = new Random();
+        int low = MIN_REQUEST_ID;
+        int high = MAX_START_ID + 1; // nextInt is exclusive on upper limit
+
+        // Generate a random start request id that >= MIN_REQUEST_ID and <= MAX_START_ID
+        int startId = random.nextInt(high - low) + low;
+        if (sDebug) {
+            Slog.d(TAG, "RequestId(): startId= " + startId);
+        }
+        sIdCounter = new AtomicInteger(startId);
+    }
+
+    // Given a list of request ids, find the index of the last request id.
+    // Note: Since the request id wraps around, the largest request id may not be
+    // the latest request id.
+    //
+    // @param requestIds List of request ids in ascending order with at least one element.
+    // @return Index of the last request id.
+    public static int getLastRequestIdIndex(List<Integer> requestIds) {
+        // If there is only one request id, return index as 0.
+        if (requestIds.size() == 1) {
+            return 0;
+        }
+
+        // We have to use a magical number to determine if a wrap has happened because
+        // the request id could be lost. The underlying assumption of MAGIC_NUMBER is that
+        // there can't be as many as MAGIC_NUMBER of fill requests in one session.
+        boolean wrapHasHappened = false;
+        int latestRequestIdIndex = -1;
+
+        for (int i = 0; i < requestIds.size() - 1; i++) {
+            if (requestIds.get(i+1) - requestIds.get(i) > MAGIC_NUMBER) {
+                wrapHasHappened = true;
+                latestRequestIdIndex = i;
+                break;
+            }
+        }
+
+        // If there was no wrap, the last request index is the last index.
+        if (!wrapHasHappened) {
+            latestRequestIdIndex = requestIds.size() - 1;
+        }
+        if (sDebug) {
+            Slog.d(TAG, "getLastRequestIdIndex(): latestRequestIdIndex = " + latestRequestIdIndex);
+        }
+        return latestRequestIdIndex;
+    }
+
+    public int nextId(boolean isSecondary) {
         // For authentication flows, there needs to be a way to know whether to retrieve the Fill
         // Response from the primary provider or the secondary provider from the requestId. A simple
         // way to achieve this is by assigning odd number request ids to secondary provider and
@@ -82,13 +129,20 @@
         int requestId;
 
         do {
-            requestId = sIdCounter.incrementAndGet() % 0xFFFF;
+            requestId = sIdCounter.incrementAndGet() % (MAX_REQUEST_ID + 1);
+            // Skip numbers smaller than MIN_REQUEST_ID to avoid possible authentication issue
+            if (requestId < MIN_REQUEST_ID) {
+                requestId = MIN_REQUEST_ID;
+            }
             sIdCounter.set(requestId);
         } while (isSecondaryProvider(requestId) != isSecondary);
+        if (sDebug) {
+            Slog.d(TAG, "nextId(): requestId = " + requestId);
+        }
         return requestId;
-  }
+    }
 
-  public static boolean isSecondaryProvider(int requestId) {
-      return requestId % 2 == 1;
-  }
+    public static boolean isSecondaryProvider(int requestId) {
+        return requestId % 2 == 1;
+    }
 }
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 494e956..c6ddc16 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -6902,17 +6902,18 @@
         return mPendingSaveUi != null && mPendingSaveUi.getState() == PendingUi.STATE_PENDING;
     }
 
+    // Return latest response index in mResponses SparseArray.
     @GuardedBy("mLock")
     private int getLastResponseIndexLocked() {
-        if (mResponses != null) {
-            List<Integer> requestIdList = new ArrayList<>();
-            final int responseCount = mResponses.size();
-            for (int i = 0; i < responseCount; i++) {
-                requestIdList.add(mResponses.keyAt(i));
-            }
-            return mRequestId.getLastRequestIdIndex(requestIdList);
+        if (mResponses == null  || mResponses.size() == 0) {
+          return -1;
         }
-        return -1;
+        List<Integer> requestIdList = new ArrayList<>();
+        final int responseCount = mResponses.size();
+        for (int i = 0; i < responseCount; i++) {
+            requestIdList.add(mResponses.keyAt(i));
+        }
+        return mRequestId.getLastRequestIdIndex(requestIdList);
     }
 
     private LogMaker newLogMaker(int category) {
diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
index afeafa4..988a213 100644
--- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
+++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
@@ -87,15 +87,6 @@
         void onSecureWindowShown(int displayId, int uid);
     }
 
-    /**
-     * For communicating when activities are blocked from entering PIP on the display by this
-     * policy controller.
-     */
-    public interface PipBlockedCallback {
-        /** Called when an activity is blocked from entering PIP. */
-        void onEnteringPipBlocked(int uid);
-    }
-
     /** Interface to listen for interception of intents. */
     public interface IntentListenerCallback {
         /** Returns true when an intent should be intercepted */
@@ -136,7 +127,6 @@
     @GuardedBy("mGenericWindowPolicyControllerLock")
     private final ArraySet<Integer> mRunningUids = new ArraySet<>();
     @Nullable private final ActivityListener mActivityListener;
-    @Nullable private final PipBlockedCallback mPipBlockedCallback;
     @Nullable private final IntentListenerCallback mIntentListenerCallback;
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     @NonNull
@@ -190,7 +180,6 @@
             @NonNull Set<ComponentName> crossTaskNavigationExemptions,
             @Nullable ComponentName permissionDialogComponent,
             @Nullable ActivityListener activityListener,
-            @Nullable PipBlockedCallback pipBlockedCallback,
             @Nullable ActivityBlockedCallback activityBlockedCallback,
             @Nullable SecureWindowCallback secureWindowCallback,
             @Nullable IntentListenerCallback intentListenerCallback,
@@ -208,7 +197,6 @@
         mActivityBlockedCallback = activityBlockedCallback;
         setInterestedWindowFlags(windowFlags, systemWindowFlags);
         mActivityListener = activityListener;
-        mPipBlockedCallback = pipBlockedCallback;
         mSecureWindowCallback = secureWindowCallback;
         mIntentListenerCallback = intentListenerCallback;
         mDisplayCategories = displayCategories;
@@ -346,6 +334,10 @@
         }
         final UserHandle activityUser =
                 UserHandle.getUserHandleForUid(activityInfo.applicationInfo.uid);
+        if (!activityUser.isSystem() && !mAllowedUsers.contains(activityUser)) {
+            logActivityLaunchBlocked("Activity launch disallowed from user " + activityUser);
+            return false;
+        }
         final ComponentName activityComponent = activityInfo.getComponentName();
         if (BLOCKED_APP_STREAMING_COMPONENT.equals(activityComponent) && activityUser.isSystem()) {
             // The error dialog alerting users that streaming is blocked is always allowed.
@@ -464,18 +456,6 @@
             return mShowTasksInHostDeviceRecents;
         }
     }
-
-    @Override
-    public boolean isEnteringPipAllowed(int uid) {
-        if (super.isEnteringPipAllowed(uid)) {
-            return true;
-        }
-        if (mPipBlockedCallback != null) {
-            mHandler.post(() -> mPipBlockedCallback.onEnteringPipBlocked(uid));
-        }
-        return false;
-    }
-
     @Override
     public @Nullable ComponentName getCustomHomeComponent() {
         return mCustomHomeComponent;
@@ -512,7 +492,6 @@
                     "virtual_devices.value_activity_blocked_count",
                     mAttributionSource.getUid());
         }
-
     }
 
     private static boolean isAllowedByPolicy(boolean allowedByDefault,
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index a72259e..3c323f9 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -186,6 +186,7 @@
     @GuardedBy("mVirtualDeviceLock")
     private final SparseArray<VirtualDisplayWrapper> mVirtualDisplays = new SparseArray<>();
     private IVirtualDeviceActivityListener mActivityListener;
+    private ActivityListener mActivityListenerAdapter = null;
     private IVirtualDeviceSoundEffectListener mSoundEffectListener;
     private final DisplayManagerGlobal mDisplayManager;
     private final DisplayManagerInternal mDisplayManagerInternal;
@@ -239,6 +240,16 @@
                     Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e);
                 }
             }
+
+            @Override
+            public void onActivityLaunchBlocked(int displayId,
+                    @NonNull ComponentName componentName, @UserIdInt int userId) {
+                try {
+                    mActivityListener.onActivityLaunchBlocked(displayId, componentName, userId);
+                } catch (RemoteException e) {
+                    Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e);
+                }
+            }
         };
     }
 
@@ -1209,6 +1220,10 @@
         final ComponentName homeComponent =
                 Flags.vdmCustomHome() ? mParams.getHomeComponent() : null;
 
+        if (mActivityListenerAdapter == null) {
+            mActivityListenerAdapter = createListenerAdapter();
+        }
+
         final GenericWindowPolicyController gwpc = new GenericWindowPolicyController(
                 WindowManager.LayoutParams.FLAG_SECURE,
                 WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
@@ -1221,8 +1236,7 @@
                         ? mParams.getBlockedCrossTaskNavigations()
                         : mParams.getAllowedCrossTaskNavigations(),
                 mPermissionDialogComponent,
-                createListenerAdapter(),
-                this::onEnteringPipBlocked,
+                mActivityListenerAdapter,
                 this::onActivityBlocked,
                 this::onSecureWindowShown,
                 this::shouldInterceptIntent,
@@ -1307,10 +1321,21 @@
     @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS)
     private void onActivityBlocked(int displayId, ActivityInfo activityInfo) {
         Intent intent = BlockedAppStreamingActivity.createIntent(activityInfo, getDisplayName());
-        mContext.startActivityAsUser(
-                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK),
-                ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle(),
-                UserHandle.SYSTEM);
+        if (!android.companion.virtualdevice.flags.Flags.activityControlApi()
+                || !Objects.equals(activityInfo.getComponentName(), intent.getComponent())) {
+            mContext.startActivityAsUser(
+                    intent.addFlags(
+                            Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK),
+                    ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle(),
+                    UserHandle.SYSTEM);
+        }
+        if (android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+            mActivityListenerAdapter.onActivityLaunchBlocked(
+                    displayId,
+                    activityInfo.getComponentName(),
+                    UserHandle.getUserHandleForUid(
+                            activityInfo.applicationInfo.uid).getIdentifier());
+        }
     }
 
     private void onSecureWindowShown(int displayId, int uid) {
@@ -1510,12 +1535,6 @@
         return mInputController.getInputDeviceDescriptors().values().stream().anyMatch(
                 inputDeviceDescriptor -> inputDeviceDescriptor.getInputDeviceId() == inputDeviceId);
     }
-
-    void onEnteringPipBlocked(int uid) {
-        // Do nothing. ActivityRecord#checkEnterPictureInPictureState logs that the display does not
-        // support PiP.
-    }
-
     void playSoundEffect(int effectType) {
         try {
             mSoundEffectListener.onPlaySoundEffect(effectType);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index d7fce9f..f6b3b39 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -5483,9 +5483,11 @@
         // without even solving the underlying issue (it merely hits the timeout).
         // This feature is disabled on TV since the ThemeOverlayController is currently not present
         // and therefore we do not want to wait unnecessarily.
+        // This feature is currently disabled in WearOS to avoid extreme boot regressions
         return enableHomeDelay()
                 && !mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
-                && !mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
+                && !mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)
+                && !mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
     }
 
     final void ensureBootCompleted() {
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 0c14a1c..00183ac 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -848,6 +848,7 @@
     }
 
     private void syncStats(String reason, int flags) {
+        mStats.collectPowerStatsSamples();
         awaitUninterruptibly(mWorker.scheduleSync(reason, flags));
     }
 
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index c094724..f4b1229 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -855,8 +855,8 @@
                         Slog.i(TAG, "Failed to connect to lmkd, retry after " +
                                 LMKD_RECONNECT_DELAY_MS + " ms");
                         // retry after LMKD_RECONNECT_DELAY_MS
-                        sKillHandler.sendMessageDelayed(sKillHandler.obtainMessage(
-                                KillHandler.LMKD_RECONNECT_MSG), LMKD_RECONNECT_DELAY_MS);
+                        sendMessageDelayed(obtainMessage(
+                                LMKD_RECONNECT_MSG), LMKD_RECONNECT_DELAY_MS);
                     }
                     break;
                 default:
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index a8c269d..cc4f7d9 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -51,6 +51,7 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.PowerManager;
+import android.os.Process;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.SystemClock;
@@ -374,6 +375,16 @@
                 deviceInfo.mScoAudioMode, deviceInfo.mIsPrivileged, deviceInfo.mEventSource);
     }
 
+    /**
+     * Indicates if a Bluetooth SCO activation request owner is controlling
+     * the SCO audio state itself or not.
+     * @param uid the UI of the SOC request owner app
+     * @return true if we should control SCO audio state, false otherwise
+     */
+    private boolean shouldStartScoForUid(int uid) {
+        return !(uid == Process.BLUETOOTH_UID || uid == Process.PHONE_UID);
+    }
+
     @GuardedBy("mDeviceStateLock")
     /*package*/ void setCommunicationRouteForClient(
                             IBinder cb, int uid, AudioDeviceAttributes device,
@@ -388,7 +399,7 @@
                                         + " device: " + device + " isPrivileged: " + isPrivileged
                                         + " from API: " + eventSource)).printLog(TAG));
 
-        final boolean wasBtScoRequested = isBluetoothScoRequested();
+        final int previousBtScoRequesterUid = bluetoothScoRequestOwnerUid();
         CommunicationRouteClient client;
 
         // Save previous client route in case of failure to start BT SCO audio
@@ -412,8 +423,40 @@
         if (client == null) {
             return;
         }
-        if (!mScoManagedByAudio) {
-            boolean isBtScoRequested = isBluetoothScoRequested();
+        final int btScoRequesterUid = bluetoothScoRequestOwnerUid();
+        final boolean isBtScoRequested = btScoRequesterUid != -1;
+        final boolean wasBtScoRequested = previousBtScoRequesterUid != -1;
+
+        if (mScoManagedByAudio) {
+            if (isBtScoRequested && (!wasBtScoRequested || !isBluetoothScoActive()
+                    || !mBtHelper.isBluetoothScoRequestedInternally())) {
+                boolean scoStarted = false;
+                if (shouldStartScoForUid(btScoRequesterUid)) {
+                    scoStarted = mBtHelper.startBluetoothSco(scoAudioMode, eventSource);
+                    if (!scoStarted) {
+                        Log.w(TAG, "setCommunicationRouteForClient: "
+                                + "failure to start BT SCO for uid: " + uid);
+                        // clean up or restore previous client selection
+                        if (prevClientDevice != null) {
+                            addCommunicationRouteClient(cb, uid, prevClientDevice, prevPrivileged);
+                        } else {
+                            removeCommunicationRouteClient(cb, true);
+                        }
+                        postBroadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+                    }
+                } else {
+                    scoStarted = true;
+                }
+                if (scoStarted) {
+                    setBluetoothScoOn(true, "setCommunicationRouteForClient");
+                }
+            } else if (!isBtScoRequested && wasBtScoRequested) {
+                if (shouldStartScoForUid(previousBtScoRequesterUid)) {
+                    mBtHelper.stopBluetoothSco(eventSource);
+                }
+                setBluetoothScoOn(false, "setCommunicationRouteForClient");
+            }
+        } else {
             if (isBtScoRequested && (!wasBtScoRequested || !isBluetoothScoActive()
                     || !mBtHelper.isBluetoothScoRequestedInternally())) {
                 if (!mBtHelper.startBluetoothSco(scoAudioMode, eventSource)) {
@@ -575,12 +618,12 @@
     @GuardedBy("mDeviceStateLock")
     /*package*/ void updateCommunicationRouteClientState(
                             CommunicationRouteClient client, boolean wasActive) {
-        boolean wasBtScoRequested = isBluetoothScoRequested();
+        int btScoRequesterUid = bluetoothScoRequestOwnerUid();
         client.setPlaybackActive(mAudioService.isPlaybackActiveForUid(client.getUid()));
         client.setRecordingActive(mAudioService.isRecordingActiveForUid(client.getUid()));
         if (wasActive != client.isActive()) {
             postUpdateCommunicationRouteClient(
-                    wasBtScoRequested, "updateCommunicationRouteClientState");
+                    btScoRequesterUid, "updateCommunicationRouteClientState");
         }
     }
 
@@ -763,6 +806,22 @@
     }
 
     /**
+     * Helper method on top of isBluetoothScoRequested() returning the UID of the
+     * BT SCO route request owner of -1 if SCO is not requested.
+     * @return the UID of the BT SCO route request owner of -1 if SCO is not requested.
+     */
+    @GuardedBy("mDeviceStateLock")
+    /*package*/ int bluetoothScoRequestOwnerUid() {
+        if (!isBluetoothScoRequested()) {
+            return -1;
+        }
+        CommunicationRouteClient crc = topCommunicationRouteClient();
+        if (crc == null) {
+            return -1;
+        }
+        return crc.getUid();
+    }
+    /**
      * Helper method on top of isDeviceRequestedForCommunication() indicating if
      * Bluetooth LE Audio communication device is currently requested or not.
      * @return true if Bluetooth LE Audio device is requested, false otherwise.
@@ -1148,15 +1207,18 @@
         }
     }
 
+    @GuardedBy("mDeviceStateLock")
     /*package*/ void setBluetoothScoOn(boolean on, String eventSource) {
         synchronized (mBluetoothAudioStateLock) {
-            boolean isBtScoRequested = isBluetoothScoRequested();
+            int btScoRequesterUId = bluetoothScoRequestOwnerUid();
             Log.i(TAG, "setBluetoothScoOn: " + on + ", mBluetoothScoOn: "
-                    + mBluetoothScoOn + ", isBtScoRequested: " + isBtScoRequested
+                    + mBluetoothScoOn + ", btScoRequesterUId: " + btScoRequesterUId
                     + ", from: " + eventSource);
             mBluetoothScoOn = on;
             updateAudioHalBluetoothState();
-            postUpdateCommunicationRouteClient(isBtScoRequested, eventSource);
+            if (!mScoManagedByAudio) {
+                postUpdateCommunicationRouteClient(btScoRequesterUId, eventSource);
+            }
         }
     }
 
@@ -1510,9 +1572,9 @@
     }
 
     /*package*/ void postUpdateCommunicationRouteClient(
-            boolean wasBtScoRequested, String eventSource) {
+            int btScoRequesterUid, String eventSource) {
         sendILMsgNoDelay(MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT, SENDMSG_QUEUE,
-                wasBtScoRequested ? 1 : 0, eventSource);
+                btScoRequesterUid, eventSource);
     }
 
     /*package*/ void postSetCommunicationDeviceForClient(CommunicationDeviceInfo info) {
@@ -1865,7 +1927,7 @@
                                         || btInfo.mProfile == BluetoothProfile.HEARING_AID
                                         || (mScoManagedByAudio
                                             && btInfo.mProfile == BluetoothProfile.HEADSET)) {
-                                    onUpdateCommunicationRouteClient(isBluetoothScoRequested(),
+                                    onUpdateCommunicationRouteClient(bluetoothScoRequestOwnerUid(),
                                             "setBluetoothActiveDevice");
                                 }
                             }
@@ -1927,11 +1989,11 @@
                 case MSG_I_SET_MODE_OWNER:
                     synchronized (mSetModeLock) {
                         synchronized (mDeviceStateLock) {
-                            boolean wasBtScoRequested = isBluetoothScoRequested();
+                            int btScoRequesterUid = bluetoothScoRequestOwnerUid();
                             mAudioModeOwner = (AudioModeInfo) msg.obj;
                             if (mAudioModeOwner.mMode != AudioSystem.MODE_RINGTONE) {
                                 onUpdateCommunicationRouteClient(
-                                        wasBtScoRequested, "setNewModeOwner");
+                                        btScoRequesterUid, "setNewModeOwner");
                             }
                         }
                     }
@@ -1958,7 +2020,7 @@
                 case MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT:
                     synchronized (mSetModeLock) {
                         synchronized (mDeviceStateLock) {
-                            onUpdateCommunicationRouteClient(msg.arg1 == 1, (String) msg.obj);
+                            onUpdateCommunicationRouteClient(msg.arg1, (String) msg.obj);
                         }
                     }
                     break;
@@ -2457,7 +2519,7 @@
     @Nullable private AudioDeviceAttributes preferredCommunicationDevice() {
         boolean btSCoOn = mBtHelper.isBluetoothScoOn();
         synchronized (mBluetoothAudioStateLock) {
-            btSCoOn = btSCoOn && mBluetoothScoOn;
+            btSCoOn = (btSCoOn || mScoManagedByAudio) && mBluetoothScoOn;
         }
 
         if (btSCoOn) {
@@ -2522,18 +2584,28 @@
      */
     // @GuardedBy("mSetModeLock")
     @GuardedBy("mDeviceStateLock")
-    private void onUpdateCommunicationRouteClient(boolean wasBtScoRequested, String eventSource) {
+    private void onUpdateCommunicationRouteClient(
+            int previousBtScoRequesterUid, String eventSource) {
         CommunicationRouteClient crc = topCommunicationRouteClient();
         if (AudioService.DEBUG_COMM_RTE) {
             Log.v(TAG, "onUpdateCommunicationRouteClient, crc: " + crc
-                    + " wasBtScoRequested: " + wasBtScoRequested + " eventSource: " + eventSource);
+                    + " previousBtScoRequesterUid: " + previousBtScoRequesterUid
+                    + " eventSource: " + eventSource);
         }
         if (crc != null) {
             setCommunicationRouteForClient(crc.getBinder(), crc.getUid(), crc.getDevice(),
                     BtHelper.SCO_MODE_UNDEFINED, crc.isPrivileged(), eventSource);
         } else {
-            if (!mScoManagedByAudio && !isBluetoothScoRequested() && wasBtScoRequested) {
-                mBtHelper.stopBluetoothSco(eventSource);
+            boolean wasScoRequested = previousBtScoRequesterUid != -1;
+            if (!isBluetoothScoRequested() && wasScoRequested) {
+                if (mScoManagedByAudio) {
+                    if (shouldStartScoForUid(previousBtScoRequesterUid)) {
+                        mBtHelper.stopBluetoothSco(eventSource);
+                    }
+                    setBluetoothScoOn(false, eventSource);
+                } else {
+                    mBtHelper.stopBluetoothSco(eventSource);
+                }
             }
             updateCommunicationRoute(eventSource);
         }
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 6ff4a61..2986340 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -2528,7 +2528,7 @@
             mDeviceBroker.setBluetoothA2dpOnInt(true, false /*fromA2dp*/, eventSource);
 
             AudioDeviceAttributes ada = new AudioDeviceAttributes(device, address, name);
-            final int res = AudioSystem.setDeviceConnectionState(ada,
+            final int res = mAudioSystem.setDeviceConnectionState(ada,
                     AudioSystem.DEVICE_STATE_AVAILABLE, codec);
             if (res != AudioSystem.AUDIO_STATUS_OK) {
                 AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
@@ -2575,7 +2575,7 @@
         AudioDeviceAttributes ada = null;
         if (device != AudioSystem.DEVICE_NONE) {
             ada = new AudioDeviceAttributes(device, address);
-            final int res = AudioSystem.setDeviceConnectionState(ada,
+            final int res = mAudioSystem.setDeviceConnectionState(ada,
                     AudioSystem.DEVICE_STATE_UNAVAILABLE,
                     codec);
 
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index b6125cc..0de3428 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -401,70 +401,59 @@
     private void onScoAudioStateChanged(int state) {
         boolean broadcast = false;
         int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR;
-        Log.i(TAG, "onScoAudioStateChanged state: " + state + " mScoAudioState: " + mScoAudioState);
-        if (mDeviceBroker.isScoManagedByAudio()) {
-            switch (state) {
-                case BluetoothHeadset.STATE_AUDIO_CONNECTED:
-                    mDeviceBroker.setBluetoothScoOn(true, "BtHelper.onScoAudioStateChanged");
-                    scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTED;
+        Log.i(TAG, "onScoAudioStateChanged  state: " + state
+                + ", mScoAudioState: " + mScoAudioState);
+        switch (state) {
+            case BluetoothHeadset.STATE_AUDIO_CONNECTED:
+                scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTED;
+                if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL
+                        && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {
+                    mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
+                } else if (mDeviceBroker.isBluetoothScoRequested()) {
+                    // broadcast intent if the connection was initated by AudioService
                     broadcast = true;
-                    break;
-                case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
-                    mDeviceBroker.setBluetoothScoOn(false, "BtHelper.onScoAudioStateChanged");
-                    scoAudioState = AudioManager.SCO_AUDIO_STATE_DISCONNECTED;
-                    broadcast = true;
-                    break;
-                default:
-                    break;
-            }
-        } else {
-            switch (state) {
-                case BluetoothHeadset.STATE_AUDIO_CONNECTED:
-                    scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTED;
-                    if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL
-                            && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {
-                        mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
-                    } else if (mDeviceBroker.isBluetoothScoRequested()) {
-                        // broadcast intent if the connection was initated by AudioService
-                        broadcast = true;
-                    }
+                }
+                if (!mDeviceBroker.isScoManagedByAudio()) {
                     mDeviceBroker.setBluetoothScoOn(true, "BtHelper.onScoAudioStateChanged");
-                    break;
-                case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
+                }
+                break;
+            case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
+                if (!mDeviceBroker.isScoManagedByAudio()) {
                     mDeviceBroker.setBluetoothScoOn(false, "BtHelper.onScoAudioStateChanged");
-                    scoAudioState = AudioManager.SCO_AUDIO_STATE_DISCONNECTED;
-                    // There are two cases where we want to immediately reconnect audio:
-                    // 1) If a new start request was received while disconnecting: this was
-                    // notified by requestScoState() setting state to SCO_STATE_ACTIVATE_REQ.
-                    // 2) If audio was connected then disconnected via Bluetooth APIs and
-                    // we still have pending activation requests by apps: this is indicated by
-                    // state SCO_STATE_ACTIVE_EXTERNAL and BT SCO is requested.
-                    if (mScoAudioState == SCO_STATE_ACTIVATE_REQ) {
-                        if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null
-                                && connectBluetoothScoAudioHelper(mBluetoothHeadset,
-                                mBluetoothHeadsetDevice, mScoAudioMode)) {
-                            mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
-                            scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTING;
-                            broadcast = true;
-                            break;
-                        }
-                    }
-                    if (mScoAudioState != SCO_STATE_ACTIVE_EXTERNAL) {
+                }
+                scoAudioState = AudioManager.SCO_AUDIO_STATE_DISCONNECTED;
+                // There are two cases where we want to immediately reconnect audio:
+                // 1) If a new start request was received while disconnecting: this was
+                // notified by requestScoState() setting state to SCO_STATE_ACTIVATE_REQ.
+                // 2) If audio was connected then disconnected via Bluetooth APIs and
+                // we still have pending activation requests by apps: this is indicated by
+                // state SCO_STATE_ACTIVE_EXTERNAL and BT SCO is requested.
+                if (mScoAudioState == SCO_STATE_ACTIVATE_REQ) {
+                    if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null
+                            && connectBluetoothScoAudioHelper(mBluetoothHeadset,
+                            mBluetoothHeadsetDevice, mScoAudioMode)) {
+                        mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
+                        scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTING;
                         broadcast = true;
+                        break;
                     }
-                    mScoAudioState = SCO_STATE_INACTIVE;
-                    break;
-                case BluetoothHeadset.STATE_AUDIO_CONNECTING:
-                    if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL
-                            && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {
-                        mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
-                    }
-                    break;
-                default:
-                    break;
-            }
+                }
+                if (mScoAudioState != SCO_STATE_ACTIVE_EXTERNAL) {
+                    broadcast = true;
+                }
+                mScoAudioState = SCO_STATE_INACTIVE;
+                break;
+            case BluetoothHeadset.STATE_AUDIO_CONNECTING:
+                if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL
+                        && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {
+                    mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
+                }
+                break;
+            default:
+                break;
         }
         if (broadcast) {
+            Log.i(TAG, "onScoAudioStateChanged  broadcasting state: " + scoAudioState);
             broadcastScoConnectionState(scoAudioState);
             //FIXME: this is to maintain compatibility with deprecated intent
             // AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED. Remove when appropriate.
diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java
index 2a16872..f5a2a21 100644
--- a/services/core/java/com/android/server/biometrics/AuthService.java
+++ b/services/core/java/com/android/server/biometrics/AuthService.java
@@ -369,6 +369,10 @@
                 checkPermission();
             }
 
+            if ((authenticators & Authenticators.MANDATORY_BIOMETRICS) != 0) {
+                checkBiometricAdvancedPermission();
+            }
+
             final long identity = Binder.clearCallingIdentity();
             try {
                 final int result = mBiometricService.canAuthenticate(
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
index daaafcb..693a3e6 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
@@ -418,7 +418,7 @@
     }
 
     protected int getRequestReason() {
-        if (isKeyguard()) {
+        if (isKeyguard() && !isBiometricPrompt()) {
             return BiometricRequestConstants.REASON_AUTH_KEYGUARD;
         } else if (isBiometricPrompt()) {
             // BP reason always takes precedent over settings, since callers from within
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
index e8394d4..619aecf 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
@@ -19,6 +19,9 @@
 import static android.Manifest.permission.CONTROL_DEVICE_STATE;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY;
+import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY;
+import static android.hardware.devicestate.DeviceState.PROPERTY_POLICY_AVAILABLE_FOR_APP_REQUEST;
 import static android.hardware.devicestate.DeviceState.PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS;
 import static android.hardware.devicestate.DeviceState.PROPERTY_POLICY_CANCEL_WHEN_REQUESTER_NOT_ON_TOP;
 import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
@@ -47,6 +50,8 @@
 import android.hardware.devicestate.DeviceStateManagerInternal;
 import android.hardware.devicestate.IDeviceStateManager;
 import android.hardware.devicestate.IDeviceStateManagerCallback;
+import android.hardware.devicestate.feature.flags.FeatureFlags;
+import android.hardware.devicestate.feature.flags.FeatureFlagsImpl;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
@@ -175,7 +180,7 @@
 
     private Set<Integer> mDeviceStatesAvailableForAppRequests = new HashSet<>();
 
-    private Set<Integer> mFoldedDeviceStates;
+    private Set<Integer> mFoldedDeviceStates = new HashSet<>();
 
     @Nullable
     private DeviceState mRearDisplayState;
@@ -185,6 +190,9 @@
     @Nullable
     private OverrideRequest mRearDisplayPendingOverrideRequest;
 
+    @NonNull
+    private final FeatureFlags mFlags;
+
     @VisibleForTesting
     interface SystemPropertySetter {
         void setDebugTracingDeviceStateProperty(String value);
@@ -245,6 +253,7 @@
             @NonNull SystemPropertySetter systemPropertySetter) {
         super(context);
         mSystemPropertySetter = systemPropertySetter;
+        mFlags = new FeatureFlagsImpl();
         // We use the DisplayThread because this service indirectly drives
         // display (on/off) and window (position) events through its callbacks.
         DisplayThread displayThread = DisplayThread.get();
@@ -270,9 +279,12 @@
         publishBinderService(Context.DEVICE_STATE_SERVICE, mBinderService);
         publishLocalService(DeviceStateManagerInternal.class, new LocalService());
 
-        synchronized (mLock) {
-            readStatesAvailableForRequestFromApps();
-            mFoldedDeviceStates = readFoldedStates();
+        if (!mFlags.deviceStatePropertyMigration()) {
+            synchronized (mLock) {
+                readStatesAvailableForRequestFromApps();
+                mFoldedDeviceStates = readFoldedStates();
+                setRearDisplayStateLocked();
+            }
         }
 
         mActivityTaskManagerInternal.registerScreenObserver(mOverrideRequestScreenObserver);
@@ -461,8 +473,6 @@
             mOverrideRequestController.handleNewSupportedStates(newStateIdentifiers, reason);
             updatePendingStateLocked();
 
-            setRearDisplayStateLocked();
-
             notifyDeviceStateInfoChangedAsync();
 
             mHandler.post(this::notifyPolicyIfNeeded);
@@ -838,12 +848,22 @@
             OverrideRequest request = new OverrideRequest(token, callingPid, callingUid,
                     deviceState.get(), flags, OVERRIDE_REQUEST_TYPE_EMULATED_STATE);
 
-            // If we don't have the CONTROL_DEVICE_STATE permission, we want to show the overlay
-            if (!hasControlDeviceStatePermission && mRearDisplayState != null
-                    && state == mRearDisplayState.getIdentifier()) {
-                showRearDisplayEducationalOverlayLocked(request);
+            if (mFlags.deviceStatePropertyMigration()) {
+                // If we don't have the CONTROL_DEVICE_STATE permission, we want to show the overlay
+                if (!hasControlDeviceStatePermission && deviceState.get().hasProperty(
+                        PROPERTY_FEATURE_REAR_DISPLAY)) {
+                    showRearDisplayEducationalOverlayLocked(request);
+                } else {
+                    mOverrideRequestController.addRequest(request);
+                }
             } else {
-                mOverrideRequestController.addRequest(request);
+                // If we don't have the CONTROL_DEVICE_STATE permission, we want to show the overlay
+                if (!hasControlDeviceStatePermission && mRearDisplayState != null
+                        && state == mRearDisplayState.getIdentifier()) {
+                    showRearDisplayEducationalOverlayLocked(request);
+                } else {
+                    mOverrideRequestController.addRequest(request);
+                }
             }
         }
     }
@@ -1034,7 +1054,13 @@
 
     private boolean isStateAvailableForAppRequests(int state) {
         synchronized (mLock) {
-            return mDeviceStatesAvailableForAppRequests.contains(state);
+            if (mFlags.deviceStatePropertyMigration()) {
+                Optional<DeviceState> deviceState =  getStateLocked(state);
+                return deviceState.isPresent() && deviceState.get().hasProperty(
+                        PROPERTY_POLICY_AVAILABLE_FOR_APP_REQUEST);
+            } else {
+                return mDeviceStatesAvailableForAppRequests.contains(state);
+            }
         }
     }
 
@@ -1096,9 +1122,20 @@
      */
     @GuardedBy("mLock")
     private boolean isDeviceOpeningLocked(int newBaseState) {
-        return mBaseState.filter(
-                deviceState -> mFoldedDeviceStates.contains(deviceState.getIdentifier())
-                        && !mFoldedDeviceStates.contains(newBaseState)).isPresent();
+        if (mFlags.deviceStatePropertyMigration()) {
+            final DeviceState currentBaseState = mBaseState.orElse(INVALID_DEVICE_STATE);
+            final DeviceState newDeviceBaseState = getStateLocked(newBaseState).orElse(
+                    INVALID_DEVICE_STATE);
+
+            return currentBaseState.hasProperty(
+                    PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY)
+                    && !newDeviceBaseState.hasProperty(
+                    PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY);
+        } else {
+            return mBaseState.filter(
+                    deviceState -> mFoldedDeviceStates.contains(deviceState.getIdentifier())
+                            && !mFoldedDeviceStates.contains(newBaseState)).isPresent();
+        }
     }
 
     private final class DeviceStateProviderListener implements DeviceStateProvider.Listener {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index de9715a..7cd9144 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -2098,7 +2098,7 @@
     private void onDisplayOffloadUnblockScreenOn(DisplayOffloadSession displayOffloadSession) {
         Message msg = mHandler.obtainMessage(MSG_OFFLOADING_SCREEN_ON_UNBLOCKED,
                 displayOffloadSession);
-        mHandler.sendMessage(msg);
+        mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
     }
 
     private void unblockScreenOnByDisplayOffload() {
@@ -2116,7 +2116,7 @@
         if (mDisplayOffloadSession == null) {
             return;
         }
-        if (mPendingScreenOnUnblockerByDisplayOffload != null) {
+        if (mPendingScreenOnUnblockerByDisplayOffload == null) {
             // Already unblocked.
             return;
         }
@@ -2134,7 +2134,6 @@
 
         // If the screen is turning on, give displayoffload a chance to do something before the
         // screen actually turns on.
-        // TODO(b/316941732): add tests for this displayoffload screen-on blocker.
         if (isOn && changed && !mScreenTurningOnWasBlockedByDisplayOffload) {
             blockScreenOnByDisplayOffload(mDisplayOffloadSession);
         } else if (!isOn && mScreenTurningOnWasBlockedByDisplayOffload) {
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index 18a9986..886857c 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -1079,6 +1079,21 @@
         }
 
         @Override // Binder call
+        public void finishSelfOneway(IBinder token, boolean immediate) {
+            // Requires no permission, called by Dream from an arbitrary process.
+            if (token == null) {
+                throw new IllegalArgumentException("token must not be null");
+            }
+
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                finishSelfInternal(token, immediate);
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+
+        @Override // Binder call
         public void startDozing(
                 IBinder token, int screenState, @Display.StateReason int reason,
                 int screenBrightness) {
@@ -1096,6 +1111,23 @@
         }
 
         @Override // Binder call
+        public void startDozingOneway(
+                IBinder token, int screenState, @Display.StateReason int reason,
+                int screenBrightness) {
+            // Requires no permission, called by Dream from an arbitrary process.
+            if (token == null) {
+                throw new IllegalArgumentException("token must not be null");
+            }
+
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                startDozingInternal(token, screenState, reason, screenBrightness);
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+
+        @Override // Binder call
         public void stopDozing(IBinder token) {
             // Requires no permission, called by Dream from an arbitrary process.
             if (token == null) {
diff --git a/services/core/java/com/android/server/health/HealthServiceWrapper.java b/services/core/java/com/android/server/health/HealthServiceWrapper.java
index 25d1a88..9c14b5b 100644
--- a/services/core/java/com/android/server/health/HealthServiceWrapper.java
+++ b/services/core/java/com/android/server/health/HealthServiceWrapper.java
@@ -71,6 +71,21 @@
     public abstract android.hardware.health.HealthInfo getHealthInfo() throws RemoteException;
 
     /**
+     * Calls into getBatteryHealthData() in the health HAL.
+     * This function does not have a corresponding HIDL implementation, so
+     * returns null by default, unless there is an AIDL class that overrides
+     * this one.
+     *
+     * @return battery health data. {@code null} if no health HAL service.
+     *     {@code null} if any service-specific error when calling {@code
+     *     getBatteryHealthData}, e.g. it is unsupported.
+     * @throws RemoteException for any transaction-level errors
+     */
+    public android.hardware.health.BatteryHealthData getBatteryHealthData() throws RemoteException {
+        return null;
+    }
+
+    /**
      * Create a new HealthServiceWrapper instance.
      *
      * @param healthInfoCallback the callback to call when health info changes
diff --git a/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java b/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java
index fd3a92e..2a3fbc3 100644
--- a/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java
+++ b/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java
@@ -212,6 +212,17 @@
         }
     }
 
+    @Override
+    public BatteryHealthData getBatteryHealthData() throws RemoteException {
+        IHealth service = mLastService.get();
+        if (service == null) return null;
+        try {
+            return service.getBatteryHealthData();
+        } catch (UnsupportedOperationException | ServiceSpecificException ex) {
+            return null;
+        }
+    }
+
     public void setChargingPolicy(int policy) throws RemoteException {
         IHealth service = mLastService.get();
         if (service == null) return;
diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java
index 38a0d37..62c21bd 100644
--- a/services/core/java/com/android/server/input/BatteryController.java
+++ b/services/core/java/com/android/server/input/BatteryController.java
@@ -83,6 +83,7 @@
     private final Handler mHandler;
     private final UEventManager mUEventManager;
     private final BluetoothBatteryManager mBluetoothBatteryManager;
+    private final Runnable mHandlePollEventCallback = this::handlePollEvent;
 
     // Maps a pid to the registered listener record for that process. There can only be one battery
     // listener per process.
@@ -206,7 +207,7 @@
         if (!mIsInteractive || !anyOf(mDeviceMonitors, DeviceMonitor::requiresPolling)) {
             // Stop polling.
             mIsPolling = false;
-            mHandler.removeCallbacks(this::handlePollEvent);
+            mHandler.removeCallbacks(mHandlePollEventCallback);
             return;
         }
 
@@ -215,7 +216,7 @@
         }
         // Start polling.
         mIsPolling = true;
-        mHandler.postDelayed(this::handlePollEvent, delayStart ? POLLING_PERIOD_MILLIS : 0);
+        mHandler.postDelayed(mHandlePollEventCallback, delayStart ? POLLING_PERIOD_MILLIS : 0);
     }
 
     private <R> R processInputDevice(int deviceId, R defaultValue, Function<InputDevice, R> func) {
@@ -366,7 +367,7 @@
             }
             final long eventTime = SystemClock.uptimeMillis();
             mDeviceMonitors.forEach((deviceId, monitor) -> monitor.onPoll(eventTime));
-            mHandler.postDelayed(this::handlePollEvent, POLLING_PERIOD_MILLIS);
+            mHandler.postDelayed(mHandlePollEventCallback, POLLING_PERIOD_MILLIS);
         }
     }
 
diff --git a/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java b/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java
index 0749edc..aeace7a 100644
--- a/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java
+++ b/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java
@@ -103,8 +103,8 @@
 
         // Note that current user ID is guaranteed to be userId.
         final var imeId = mBindingController.getSelectedMethodId();
-        final InputMethodInfo imi = InputMethodSettingsRepository.get(mBindingController.mUserId)
-                .getMethodMap().get(imeId);
+        final InputMethodInfo imi = InputMethodSettingsRepository.get(
+                mBindingController.getUserId()).getMethodMap().get(imeId);
         if (imi == null || !isInlineSuggestionsEnabled(imi, touchExplorationEnabled)) {
             callback.onInlineSuggestionsUnsupported();
             return;
diff --git a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
index b77f47d..356bc40 100644
--- a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
+++ b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
@@ -53,10 +53,10 @@
 import java.util.Objects;
 
 /**
- * The default implementation of {@link ImeVisibilityApplier} used in
- * {@link InputMethodManagerService}.
+ * A stateless helper class for IME visibility operations like show/hide and update Z-ordering
+ * relative to the IME targeted window.
  */
-final class DefaultImeVisibilityApplier implements ImeVisibilityApplier {
+final class DefaultImeVisibilityApplier {
 
     private static final String TAG = "DefaultImeVisibilityApplier";
 
@@ -75,12 +75,22 @@
         mImeTargetVisibilityPolicy = LocalServices.getService(ImeTargetVisibilityPolicy.class);
     }
 
+    /**
+     * Performs showing IME on top of the given window.
+     *
+     * @param showInputToken a token that represents the requester to show IME
+     * @param statsToken     the token tracking the current IME request
+     * @param resultReceiver if non-null, this will be called back to the caller when
+     *                       it has processed request to tell what it has done
+     * @param reason         yhe reason for requesting to show IME
+     * @param userId         the target user when performing show IME
+     */
     @GuardedBy("ImfLock.class")
-    @Override
-    public void performShowIme(IBinder showInputToken, @NonNull ImeTracker.Token statsToken,
+    void performShowIme(IBinder showInputToken, @NonNull ImeTracker.Token statsToken,
             @InputMethod.ShowFlags int showFlags, ResultReceiver resultReceiver,
             @SoftInputShowHideReason int reason, @UserIdInt int userId) {
         final var bindingController = mService.getInputMethodBindingController(userId);
+        final var userData = mService.getUserData(userId);
         final IInputMethodInvoker curMethod = bindingController.getCurMethod();
         if (curMethod != null) {
             if (DEBUG) {
@@ -93,10 +103,10 @@
                 if (DEBUG_IME_VISIBILITY) {
                     EventLog.writeEvent(IMF_SHOW_IME,
                             statsToken != null ? statsToken.getTag() : ImeTracker.TOKEN_NONE,
-                            Objects.toString(mService.mImeBindingState.mFocusedWindow),
+                            Objects.toString(userData.mImeBindingState.mFocusedWindow),
                             InputMethodDebug.softInputDisplayReasonToString(reason),
                             InputMethodDebug.softInputModeToString(
-                                    mService.mImeBindingState.mFocusedWindowSoftInputMode));
+                                    userData.mImeBindingState.mFocusedWindowSoftInputMode));
                 }
                 mService.onShowHideSoftInputRequested(true /* show */, showInputToken, reason,
                         statsToken, userId);
@@ -104,13 +114,23 @@
         }
     }
 
+    /**
+     * Performs hiding IME to the given window
+     *
+     * @param hideInputToken a token that represents the requester to hide IME
+     * @param statsToken     the token tracking the current IME request
+     * @param resultReceiver if non-null, this will be called back to the caller when
+     *                       it has processed request to tell what it has done
+     * @param reason         the reason for requesting to hide IME
+     * @param userId         the target user when performing hide IME
+     */
     @GuardedBy("ImfLock.class")
-    @Override
-    public void performHideIme(IBinder hideInputToken, @NonNull ImeTracker.Token statsToken,
+    void performHideIme(IBinder hideInputToken, @NonNull ImeTracker.Token statsToken,
             ResultReceiver resultReceiver, @SoftInputShowHideReason int reason,
             @UserIdInt int userId) {
         final var bindingController = mService.getInputMethodBindingController(userId);
         final IInputMethodInvoker curMethod = bindingController.getCurMethod();
+        final var userData = mService.getUserData(userId);
         if (curMethod != null) {
             // The IME will report its visible state again after the following message finally
             // delivered to the IME process as an IPC.  Hence the inconsistency between
@@ -126,10 +146,10 @@
                 if (DEBUG_IME_VISIBILITY) {
                     EventLog.writeEvent(IMF_HIDE_IME,
                             statsToken != null ? statsToken.getTag() : ImeTracker.TOKEN_NONE,
-                            Objects.toString(mService.mImeBindingState.mFocusedWindow),
+                            Objects.toString(userData.mImeBindingState.mFocusedWindow),
                             InputMethodDebug.softInputDisplayReasonToString(reason),
                             InputMethodDebug.softInputModeToString(
-                                    mService.mImeBindingState.mFocusedWindowSoftInputMode));
+                                    userData.mImeBindingState.mFocusedWindowSoftInputMode));
                 }
                 mService.onShowHideSoftInputRequested(false /* show */, hideInputToken, reason,
                         statsToken, userId);
@@ -137,14 +157,16 @@
         }
     }
 
-    @GuardedBy("ImfLock.class")
-    @Override
-    public void applyImeVisibility(IBinder windowToken, @NonNull ImeTracker.Token statsToken,
-            @ImeVisibilityStateComputer.VisibilityState int state, @UserIdInt int userId) {
-        applyImeVisibility(windowToken, statsToken, state,
-                SoftInputShowHideReason.NOT_SET /* ignore reason */, userId);
-    }
-
+    /**
+     * Applies the IME visibility from {@link android.inputmethodservice.InputMethodService} with
+     * according to the given visibility state.
+     *
+     * @param windowToken the token of a window for applying the IME visibility
+     * @param statsToken  the token tracking the current IME request
+     * @param state       the new IME visibility state for the applier to handle
+     * @param reason      one of {@link SoftInputShowHideReason}
+     * @param userId      the target user when applying the IME visibility state
+     */
     @GuardedBy("ImfLock.class")
     void applyImeVisibility(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
             @ImeVisibilityStateComputer.VisibilityState int state,
@@ -179,29 +201,30 @@
                 break;
             case STATE_HIDE_IME_EXPLICIT:
                 if (Flags.refactorInsetsController()) {
-                    setImeVisibilityOnFocusedWindowClient(false);
+                    setImeVisibilityOnFocusedWindowClient(false, userId);
                 } else {
                     mService.hideCurrentInputLocked(windowToken, statsToken,
-                            0 /* flags */, null /* resultReceiver */, reason);
+                            0 /* flags */, null /* resultReceiver */, reason, userId);
                 }
                 break;
             case STATE_HIDE_IME_NOT_ALWAYS:
                 if (Flags.refactorInsetsController()) {
-                    setImeVisibilityOnFocusedWindowClient(false);
+                    setImeVisibilityOnFocusedWindowClient(false, userId);
                 } else {
                     mService.hideCurrentInputLocked(windowToken, statsToken,
-                            InputMethodManager.HIDE_NOT_ALWAYS, null /* resultReceiver */, reason);
+                            InputMethodManager.HIDE_NOT_ALWAYS, null /* resultReceiver */, reason,
+                            userId);
                 }
                 break;
             case STATE_SHOW_IME_IMPLICIT:
                 if (Flags.refactorInsetsController()) {
                     // This can be triggered by IMMS#startInputOrWindowGainedFocus. We need to
                     // set the requestedVisibleTypes in InsetsController first, before applying it.
-                    setImeVisibilityOnFocusedWindowClient(true);
+                    setImeVisibilityOnFocusedWindowClient(true, userId);
                 } else {
                     mService.showCurrentInputLocked(windowToken, statsToken,
                             InputMethodManager.SHOW_IMPLICIT, MotionEvent.TOOL_TYPE_UNKNOWN,
-                            null /* resultReceiver */, reason);
+                            null /* resultReceiver */, reason, userId);
                 }
                 break;
             case STATE_SHOW_IME_SNAPSHOT:
@@ -215,9 +238,16 @@
         }
     }
 
+    /**
+     * Shows the IME screenshot and attach it to the given IME target window.
+     *
+     * @param imeTarget   the token of a window to show the IME screenshot
+     * @param displayId   the unique id to identify the display
+     * @param userId      the target user when when showing the IME screenshot
+     * @return {@code true} if success, {@code false} otherwise
+     */
     @GuardedBy("ImfLock.class")
-    @Override
-    public boolean showImeScreenshot(@NonNull IBinder imeTarget, int displayId,
+    boolean showImeScreenshot(@NonNull IBinder imeTarget, int displayId,
             @UserIdInt int userId) {
         if (mImeTargetVisibilityPolicy.showImeScreenshot(imeTarget, displayId)) {
             mService.onShowHideSoftInputRequested(false /* show */, imeTarget,
@@ -227,23 +257,32 @@
         return false;
     }
 
+    /**
+     * Removes the IME screenshot on the given display.
+     *
+     * @param displayId the target display of showing IME screenshot
+     * @param userId    the target user of showing IME screenshot
+     * @return {@code true} if success, {@code false} otherwise
+     */
     @GuardedBy("ImfLock.class")
-    @Override
-    public boolean removeImeScreenshot(int displayId, @UserIdInt int userId) {
+    boolean removeImeScreenshot(int displayId, @UserIdInt int userId) {
+        final var userData = mService.getUserData(userId);
         if (mImeTargetVisibilityPolicy.removeImeScreenshot(displayId)) {
             mService.onShowHideSoftInputRequested(false /* show */,
-                    mService.mImeBindingState.mFocusedWindow,
+                    userData.mImeBindingState.mFocusedWindow,
                     REMOVE_IME_SCREENSHOT_FROM_IMMS, null /* statsToken */, userId);
             return true;
         }
         return false;
     }
 
-    private void setImeVisibilityOnFocusedWindowClient(boolean visibility) {
-        if (mService.mImeBindingState != null
-                && mService.mImeBindingState.mFocusedWindowClient != null
-                && mService.mImeBindingState.mFocusedWindowClient.mClient != null) {
-            mService.mImeBindingState.mFocusedWindowClient.mClient.setImeVisibility(visibility);
+    @GuardedBy("ImfLock.class")
+    private void setImeVisibilityOnFocusedWindowClient(boolean visibility, @UserIdInt int userId) {
+        final var userData = mService.getUserData(userId);
+        if (userData.mImeBindingState != null
+                && userData.mImeBindingState.mFocusedWindowClient != null
+                && userData.mImeBindingState.mFocusedWindowClient.mClient != null) {
+            userData.mImeBindingState.mFocusedWindowClient.mClient.setImeVisibility(visibility);
         } else {
             // TODO(b/329229469): ImeTracker?
         }
diff --git a/services/core/java/com/android/server/inputmethod/ImeTrackerService.java b/services/core/java/com/android/server/inputmethod/ImeTrackerService.java
index 56fa8c9..14551a1 100644
--- a/services/core/java/com/android/server/inputmethod/ImeTrackerService.java
+++ b/services/core/java/com/android/server/inputmethod/ImeTrackerService.java
@@ -23,7 +23,6 @@
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
-import android.os.Looper;
 import android.util.Log;
 import android.view.inputmethod.ImeTracker;
 
@@ -70,8 +69,8 @@
 
     private final Object mLock = new Object();
 
-    ImeTrackerService(@NonNull Looper looper) {
-        mHandler = new Handler(looper, null /* callback */, true /* async */);
+    ImeTrackerService(@NonNull Handler handler) {
+        mHandler = handler;
     }
 
     @NonNull
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
deleted file mode 100644
index c1069f2..0000000
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.inputmethod;
-
-import android.annotation.NonNull;
-import android.annotation.UserIdInt;
-import android.os.IBinder;
-import android.os.ResultReceiver;
-import android.view.inputmethod.ImeTracker;
-import android.view.inputmethod.InputMethod;
-
-import com.android.internal.inputmethod.SoftInputShowHideReason;
-
-/**
- * Interface for IME visibility operations like show/hide and update Z-ordering relative to the IME
- * targeted window.
- */
-interface ImeVisibilityApplier {
-    /**
-     * Performs showing IME on top of the given window.
-     *
-     * @param showInputToken a token that represents the requester to show IME
-     * @param statsToken     the token tracking the current IME request
-     * @param resultReceiver if non-null, this will be called back to the caller when
-     *                       it has processed request to tell what it has done
-     * @param reason         yhe reason for requesting to show IME
-     * @param userId         the target user when performing show IME
-     */
-    default void performShowIme(IBinder showInputToken, @NonNull ImeTracker.Token statsToken,
-            @InputMethod.ShowFlags int showFlags, ResultReceiver resultReceiver,
-            @SoftInputShowHideReason int reason, @UserIdInt int userId) {
-    }
-
-    /**
-     * Performs hiding IME to the given window
-     *
-     * @param hideInputToken a token that represents the requester to hide IME
-     * @param statsToken     the token tracking the current IME request
-     * @param resultReceiver if non-null, this will be called back to the caller when
-     *                       it has processed request to tell what it has done
-     * @param reason         the reason for requesting to hide IME
-     * @param userId         the target user when performing hide IME
-     */
-    default void performHideIme(IBinder hideInputToken, @NonNull ImeTracker.Token statsToken,
-            ResultReceiver resultReceiver, @SoftInputShowHideReason int reason,
-            @UserIdInt int userId) {
-    }
-
-    /**
-     * Applies the IME visibility from {@link android.inputmethodservice.InputMethodService} with
-     * according to the given visibility state.
-     *
-     * @param windowToken the token of a window for applying the IME visibility
-     * @param statsToken  the token tracking the current IME request
-     * @param state       the new IME visibility state for the applier to handle
-     * @param userId      the target user when applying the IME visibility state
-     */
-    default void applyImeVisibility(IBinder windowToken, @NonNull ImeTracker.Token statsToken,
-            @ImeVisibilityStateComputer.VisibilityState int state, @UserIdInt int userId) {
-    }
-
-    /**
-     * Updates the IME Z-ordering relative to the given window.
-     *
-     * This used to adjust the IME relative layer of the window during
-     * {@link InputMethodManagerService} is in switching IME clients.
-     *
-     * @param windowToken the token of a window to update the Z-ordering relative to the IME
-     */
-    default void updateImeLayeringByTarget(IBinder windowToken) {
-        // TODO: add a method in WindowManagerInternal to call DC#updateImeInputAndControlTarget
-        //  here to end up updating IME layering after IMMS#attachNewInputLocked called.
-    }
-
-    /**
-     * Shows the IME screenshot and attach it to the given IME target window.
-     *
-     * @param windowToken the token of a window to show the IME screenshot
-     * @param displayId   the unique id to identify the display
-     * @param userId      the target user when when showing the IME screenshot
-     * @return {@code true} if success, {@code false} otherwise
-     */
-    default boolean showImeScreenshot(@NonNull IBinder windowToken, int displayId,
-            @UserIdInt int userId) {
-        return false;
-    }
-
-    /**
-     * Removes the IME screenshot on the given display.
-     *
-     * @param displayId the target display of showing IME screenshot
-     * @param userId    the target user of showing IME screenshot
-     * @return {@code true} if success, {@code false} otherwise
-     */
-    default boolean removeImeScreenshot(int displayId, @UserIdInt int userId) {
-        return false;
-    }
-}
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
index 9d80844..7ebf595 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
@@ -38,6 +38,7 @@
 import android.accessibilityservice.AccessibilityService;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.UserIdInt;
 import android.content.res.Configuration;
 import android.os.Binder;
 import android.os.IBinder;
@@ -52,6 +53,7 @@
 import android.view.inputmethod.InputMethod;
 import android.view.inputmethod.InputMethodManager;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.inputmethod.SoftInputShowHideReason;
 import com.android.server.LocalServices;
@@ -553,15 +555,17 @@
         return null;
     }
 
-    IBinder getWindowTokenFrom(IBinder requestImeToken) {
+    @GuardedBy("ImfLock.class")
+    IBinder getWindowTokenFrom(IBinder requestImeToken, @UserIdInt int userId) {
         for (IBinder windowToken : mRequestWindowStateMap.keySet()) {
             final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken);
             if (state.getRequestImeToken() == requestImeToken) {
                 return windowToken;
             }
         }
+        final var userData = mService.getUserData(userId);
         // Fallback to the focused window for some edge cases (e.g. relaunching the activity)
-        return mService.mImeBindingState.mFocusedWindow;
+        return userData.mImeBindingState.mFocusedWindow;
     }
 
     IBinder getWindowTokenFrom(ImeTargetWindowState windowState) {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
index 60d647d..e1aa3a2 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
@@ -70,7 +70,7 @@
     /** Time in milliseconds that the IME service has to bind before it is reconnected. */
     static final long TIME_TO_RECONNECT = 3 * 1000;
 
-    @UserIdInt final int mUserId;
+    @UserIdInt private final int mUserId;
     @NonNull private final InputMethodManagerService mService;
     @NonNull private final Context mContext;
     @NonNull private final AutofillSuggestionsController mAutofillController;
@@ -382,9 +382,9 @@
                         InputMethodManager
                                 .invalidateLocalConnectionlessStylusHandwritingAvailabilityCaches();
                     }
-                    mService.initializeImeLocked(mCurMethod, mCurToken);
+                    mService.initializeImeLocked(mCurMethod, mCurToken, mUserId);
                     mService.scheduleNotifyImeUidToAudioService(mCurMethodUid);
-                    mService.reRequestCurrentClientSessionLocked();
+                    mService.reRequestCurrentClientSessionLocked(mUserId);
                     mAutofillController.performOnCreateInlineSuggestionsRequest();
                 }
 
@@ -437,7 +437,7 @@
                     mLastBindTime = SystemClock.uptimeMillis();
                     clearCurMethodAndSessions();
                     mService.clearInputShownLocked();
-                    mService.unbindCurrentClientLocked(UnbindReason.DISCONNECT_IME);
+                    mService.unbindCurrentClientLocked(UnbindReason.DISCONNECT_IME, mUserId);
                 }
             }
         }
@@ -483,7 +483,7 @@
 
     @GuardedBy("ImfLock.class")
     private void clearCurMethodAndSessions() {
-        mService.clearClientSessionsLocked();
+        mService.clearClientSessionsLocked(this);
         mCurMethod = null;
         mCurMethodUid = Process.INVALID_UID;
     }
@@ -657,4 +657,9 @@
     int getDeviceIdToShowIme() {
         return mDeviceIdToShowIme;
     }
+
+    @UserIdInt
+    int getUserId() {
+        return mUserId;
+    }
 }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index a089331..1e76324 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -90,7 +90,6 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.LocaleList;
-import android.os.Looper;
 import android.os.Message;
 import android.os.Process;
 import android.os.RemoteException;
@@ -468,12 +467,6 @@
     private final ClientController mClientController;
 
     /**
-     * Holds the current IME binding state info.
-     */
-    @MultiUserUnawareField
-    ImeBindingState mImeBindingState;
-
-    /**
      * Set once the system is ready to run third party code.
      */
     @SharedByAllUsersField
@@ -492,25 +485,6 @@
     }
 
 
-    /**
-     * Id obtained with {@link InputMethodInfo#getId()} for the currently selected input method.
-     * This is to be synchronized with the secure settings keyed with
-     * {@link Settings.Secure#DEFAULT_INPUT_METHOD}.
-     *
-     * <p>This can be transiently {@code null} when the system is re-initializing input method
-     * settings, e.g., the system locale is just changed.</p>
-     *
-     * <p>Note that {@link InputMethodBindingController#getCurId()} is used to track which IME
-     * is being connected to {@link InputMethodManagerService}.</p>
-     *
-     * @see InputMethodBindingController#getCurId()
-     */
-    @GuardedBy("ImfLock.class")
-    @Nullable
-    String getSelectedMethodIdLocked() {
-        return getInputMethodBindingController(mCurrentUserId).getSelectedMethodId();
-    }
-
     @GuardedBy("ImfLock.class")
     @Nullable
     InputMethodInfo queryInputMethodForCurrentUserLocked(@NonNull String imeId) {
@@ -518,13 +492,6 @@
     }
 
     /**
-     * The client that is currently bound to an input method.
-     */
-    @MultiUserUnawareField
-    @Nullable
-    private ClientState mCurClient;
-
-    /**
      * The last window token that we confirmed that IME started talking to.  This is always updated
      * upon reports from the input method.  If the window state is already changed before the report
      * is handled, this field just keeps the last value.
@@ -533,33 +500,6 @@
     IBinder mLastImeTargetWindow;
 
     /**
-     * The {@link IRemoteInputConnection} last provided by the current client.
-     */
-    @MultiUserUnawareField
-    IRemoteInputConnection mCurInputConnection;
-
-    /**
-     * The {@link ImeOnBackInvokedDispatcher} last provided by the current client to
-     * receive {@link android.window.OnBackInvokedCallback}s forwarded from IME.
-     */
-    @MultiUserUnawareField
-    ImeOnBackInvokedDispatcher mCurImeDispatcher;
-
-    /**
-     * The {@link IRemoteAccessibilityInputConnection} last provided by the current client.
-     */
-    @MultiUserUnawareField
-    @Nullable
-    IRemoteAccessibilityInputConnection mCurRemoteAccessibilityInputConnection;
-
-    /**
-     * The {@link EditorInfo} last provided by the current client.
-     */
-    @MultiUserUnawareField
-    @Nullable
-    EditorInfo mCurEditorInfo;
-
-    /**
      * Map of window perceptible states indexed by their associated window tokens.
      *
      * The value {@code true} indicates that IME has not been mostly hidden via
@@ -570,30 +510,6 @@
     private final WeakHashMap<IBinder, Boolean> mFocusedWindowPerceptible = new WeakHashMap<>();
 
     /**
-     * The token tracking the current IME show request that is waiting for a connection to an IME,
-     * otherwise {@code null}.
-     */
-    @Nullable
-    @MultiUserUnawareField
-    private ImeTracker.Token mCurStatsToken;
-
-    /**
-     * {@code true} if the current input method is in fullscreen mode.
-     */
-    @MultiUserUnawareField
-    boolean mInFullscreenMode;
-
-    /**
-     * The token we have made for the currently active input method, to
-     * identify it in the future.
-     */
-    @GuardedBy("ImfLock.class")
-    @Nullable
-    IBinder getCurTokenLocked() {
-        return getInputMethodBindingController(mCurrentUserId).getCurToken();
-    }
-
-    /**
      * The displayId of current active input method.
      */
     @GuardedBy("ImfLock.class")
@@ -618,27 +534,6 @@
     }
 
     /**
-     * Have we called mCurMethod.bindInput()?
-     */
-    @MultiUserUnawareField
-    boolean mBoundToMethod;
-
-    /**
-     * Have we called bindInput() for accessibility services?
-     */
-    @MultiUserUnawareField
-    boolean mBoundToAccessibility;
-
-    /**
-     * Currently enabled session.
-     */
-    @GuardedBy("ImfLock.class")
-    @MultiUserUnawareField
-    SessionState mEnabledSession;
-    @MultiUserUnawareField
-    SparseArray<AccessibilitySessionState> mEnabledAccessibilitySessions = new SparseArray<>();
-
-    /**
      * True if the device is currently interactive with user.  The value is true initially.
      */
     @MultiUserUnawareField
@@ -763,13 +658,15 @@
                             Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, 0, mUserId);
                     mVisibilityStateComputer.getImePolicy().setA11yRequestNoSoftKeyboard(
                             accessibilitySoftKeyboardSetting);
+                    final var userData = getUserData(mUserId);
                     if (mVisibilityStateComputer.getImePolicy().isA11yRequestNoSoftKeyboard()) {
-                        hideCurrentInputLocked(mImeBindingState.mFocusedWindow, 0 /* flags */,
-                                SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE);
-                    } else if (isShowRequestedForCurrentWindow()) {
-                        showCurrentInputLocked(mImeBindingState.mFocusedWindow,
+                        hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
+                                0 /* flags */, SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE,
+                                mUserId);
+                    } else if (isShowRequestedForCurrentWindow(mUserId)) {
+                        showCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
                                 InputMethodManager.SHOW_IMPLICIT,
-                                SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE);
+                                SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE, mUserId);
                     }
                 } else if (stylusHandwritingEnabledUri.equals(uri)) {
                     InputMethodManager.invalidateLocalStylusHandwritingAvailabilityCaches();
@@ -777,13 +674,13 @@
                             .invalidateLocalConnectionlessStylusHandwritingAvailabilityCaches();
                 } else {
                     boolean enabledChanged = false;
-                    String newEnabled = InputMethodSettingsRepository.get(mCurrentUserId)
+                    String newEnabled = InputMethodSettingsRepository.get(mUserId)
                             .getEnabledInputMethodsStr();
                     if (!mLastEnabled.equals(newEnabled)) {
                         mLastEnabled = newEnabled;
                         enabledChanged = true;
                     }
-                    updateInputMethodsFromSettingsLocked(enabledChanged);
+                    updateInputMethodsFromSettingsLocked(enabledChanged, mUserId);
                 }
             }
         }
@@ -846,10 +743,12 @@
                         DirectBootAwareness.AUTO);
                 InputMethodSettingsRepository.put(userId, settings);
             }
-            postInputMethodSettingUpdatedLocked(true /* resetDefaultEnabledIme */);
+            // TODO(b/305849394): Dispatch this to non-current users.
+            final int userId = mCurrentUserId;
+            postInputMethodSettingUpdatedLocked(true /* resetDefaultEnabledIme */, userId);
             // If the locale is changed, needs to reset the default ime
-            resetDefaultImeLocked(mContext);
-            updateFromSettingsLocked(true);
+            resetDefaultImeLocked(mContext, userId);
+            updateFromSettingsLocked(true, userId);
         }
     }
 
@@ -1004,7 +903,7 @@
                 if (!isCurrentUser) {
                     return;
                 }
-                postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */);
+                postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId);
 
                 boolean changed = false;
 
@@ -1046,7 +945,7 @@
                 }
 
                 if (changed) {
-                    updateFromSettingsLocked(false);
+                    updateFromSettingsLocked(false, userId);
                 }
             }
         }
@@ -1188,8 +1087,8 @@
             InputMethodSettingsRepository.put(userId, newSettings);
             if (mCurrentUserId == userId) {
                 // We need to rebuild IMEs.
-                postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */);
-                updateInputMethodsFromSettingsLocked(true /* enabledChanged */);
+                postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId);
+                updateInputMethodsFromSettingsLocked(true /* enabledChanged */, userId);
             } else if (mExperimentalConcurrentMultiUserModeEnabled) {
                 experimentalInitializeVisibleBackgroundUserLocked(userId);
             }
@@ -1208,8 +1107,9 @@
         }
         // Hide soft input before user switch task since switch task may block main handler a while
         // and delayed the hideCurrentInputLocked().
-        hideCurrentInputLocked(mImeBindingState.mFocusedWindow, 0 /* flags */,
-                SoftInputShowHideReason.HIDE_SWITCH_USER);
+        final var userData = getUserData(userId);
+        hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */,
+                SoftInputShowHideReason.HIDE_SWITCH_USER, userId);
         final UserSwitchHandlerTask task = new UserSwitchHandlerTask(this, userId,
                 clientToBeReset);
         mUserSwitchHandlerTask = task;
@@ -1258,8 +1158,7 @@
                 mIoHandler = Handler.createAsync(ioThread.getLooper());
             }
             SystemLocaleWrapper.onStart(context, this::onActionLocaleChanged, mHandler);
-            mImeTrackerService = new ImeTrackerService(serviceThreadForTesting != null
-                    ? serviceThreadForTesting.getLooper() : Looper.getMainLooper());
+            mImeTrackerService = new ImeTrackerService(mHandler);
             // Note: SettingsObserver doesn't register observers in its constructor.
             mSettingsObserver = new SettingsObserver(mHandler);
             mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
@@ -1301,7 +1200,6 @@
 
             mClientController = new ClientController(mPackageManagerInternal);
             mClientController.addClientControllerCallback(c -> onClientRemoved(c));
-            mImeBindingState = ImeBindingState.newEmptyState();
 
             mPreventImeStartupUnlessTextEditor = mRes.getBoolean(
                     com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor);
@@ -1350,10 +1248,11 @@
     }
 
     @GuardedBy("ImfLock.class")
-    private void resetDefaultImeLocked(Context context) {
+    private void resetDefaultImeLocked(Context context, @UserIdInt int userId) {
+        final var bindingController = getInputMethodBindingController(userId);
         // Do not reset the default (current) IME when it is a 3rd-party IME
-        String selectedMethodId = getSelectedMethodIdLocked();
-        final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId);
+        String selectedMethodId = bindingController.getSelectedMethodId();
+        final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
         if (selectedMethodId != null
                 && !settings.getMethodMap().get(selectedMethodId).isSystem()) {
             return;
@@ -1368,7 +1267,7 @@
         if (DEBUG) {
             Slog.i(TAG, "Default found, using " + defIm.getId());
         }
-        setSelectedInputMethodAndSubtypeLocked(defIm, NOT_A_SUBTYPE_ID, false);
+        setSelectedInputMethodAndSubtypeLocked(defIm, NOT_A_SUBTYPE_ID, false, userId);
     }
 
     @GuardedBy("ImfLock.class")
@@ -1413,22 +1312,23 @@
     @GuardedBy("ImfLock.class")
     private void switchUserOnHandlerLocked(@UserIdInt int newUserId,
             IInputMethodClientInvoker clientToBeReset) {
+        final int prevUserId = mCurrentUserId;
         if (DEBUG) {
             Slog.d(TAG, "Switching user stage 1/3. newUserId=" + newUserId
-                    + " currentUserId=" + mCurrentUserId);
+                    + " prevUserId=" + prevUserId);
         }
 
         // Clean up stuff for mCurrentUserId, which soon becomes the previous user.
 
         // TODO(b/338461930): Check if this is still necessary or not.
-        onUnbindCurrentMethodByReset();
+        onUnbindCurrentMethodByReset(prevUserId);
 
         // Note that in b/197848765 we want to see if we can keep the binding alive for better
         // profile switching.
-        final var bindingController = getInputMethodBindingController(mCurrentUserId);
+        final var bindingController = getInputMethodBindingController(prevUserId);
         bindingController.unbindCurrentMethod();
 
-        unbindCurrentClientLocked(UnbindReason.SWITCH_USER);
+        unbindCurrentClientLocked(UnbindReason.SWITCH_USER, prevUserId);
 
         // Hereafter we start initializing things for "newUserId".
 
@@ -1438,6 +1338,7 @@
         mSettingsObserver.registerContentObserverLocked(newUserId);
 
         mCurrentUserId = newUserId;
+        final var newUserData = getUserData(newUserId);
         final String defaultImiId = SecureSettingsWrapper.getString(
                 Settings.Secure.DEFAULT_INPUT_METHOD, null, newUserId);
 
@@ -1454,13 +1355,14 @@
         final boolean initialUserSwitch = TextUtils.isEmpty(defaultImiId);
 
         final InputMethodSettings newSettings = InputMethodSettingsRepository.get(newUserId);
-        postInputMethodSettingUpdatedLocked(initialUserSwitch /* resetDefaultEnabledIme */);
+        postInputMethodSettingUpdatedLocked(initialUserSwitch /* resetDefaultEnabledIme */,
+                newUserId);
         if (TextUtils.isEmpty(newSettings.getSelectedInputMethod())) {
             // This is the first time of the user switch and
             // set the current ime to the proper one.
-            resetDefaultImeLocked(mContext);
+            resetDefaultImeLocked(mContext, newUserId);
         }
-        updateFromSettingsLocked(true);
+        updateFromSettingsLocked(true, newUserId);
 
         if (initialUserSwitch) {
             InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
@@ -1479,7 +1381,7 @@
                 // The client is already gone.
                 return;
             }
-            cs.mClient.scheduleStartInputIfNecessary(mInFullscreenMode);
+            cs.mClient.scheduleStartInputIfNecessary(newUserData.mInFullscreenMode);
         }
     }
 
@@ -1541,8 +1443,8 @@
                         DirectBootAwareness.AUTO);
                 InputMethodSettingsRepository.put(currentUserId, newSettings);
                 postInputMethodSettingUpdatedLocked(
-                        !imeSelectedOnBoot /* resetDefaultEnabledIme */);
-                updateFromSettingsLocked(true);
+                        !imeSelectedOnBoot /* resetDefaultEnabledIme */, currentUserId);
+                updateFromSettingsLocked(true, currentUserId);
                 InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
                         getPackageManagerForUser(mContext, currentUserId),
                         newSettings.getEnabledInputMethodList());
@@ -1579,14 +1481,16 @@
      * Returns true iff the caller is identified to be the current input method with the token.
      *
      * @param token the window token given to the input method when it was started
+     * @param userId userId of the calling IME process
      * @return true if and only if non-null valid token is specified
      */
     @GuardedBy("ImfLock.class")
-    private boolean calledWithValidTokenLocked(@NonNull IBinder token) {
+    private boolean calledWithValidTokenLocked(@NonNull IBinder token, @UserIdInt int userId) {
         if (token == null) {
             throw new InvalidParameterException("token must not be null.");
         }
-        if (token != getCurTokenLocked()) {
+        final var bindingController = getInputMethodBindingController(userId);
+        if (token != bindingController.getCurToken()) {
             Slog.e(TAG, "Ignoring " + Debug.getCaller() + " due to an invalid token."
                     + " uid:" + Binder.getCallingUid() + " token:" + token);
             return false;
@@ -1859,20 +1763,31 @@
         }
     }
 
+    @GuardedBy("ImfLock.class")
+    private void onClientRemoved(ClientState client) {
+        clearClientSessionLocked(client);
+        clearClientSessionForAccessibilityLocked(client);
+        // TODO(b/324907325): Remove the suppress warnings once b/324907325 is fixed.
+        @SuppressWarnings("GuardedBy") Consumer<UserDataRepository.UserData> clientRemovedForUser =
+                userData -> onClientRemovedInternalLocked(client, userData);
+        mUserDataRepository.forAllUserData(clientRemovedForUser);
+    }
+
     /**
      * Hide the IME if the removed user is the current user.
      */
     // TODO(b/325515685): Move this method to InputMethodBindingController
     @GuardedBy("ImfLock.class")
-    private void onClientRemoved(ClientState client) {
-        clearClientSessionLocked(client);
-        clearClientSessionForAccessibilityLocked(client);
-        if (mCurClient == client) {
-            hideCurrentInputLocked(mImeBindingState.mFocusedWindow, 0 /* flags */,
-                    SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
-            if (mBoundToMethod) {
-                mBoundToMethod = false;
-                IInputMethodInvoker curMethod = getCurMethodLocked();
+    private void onClientRemovedInternalLocked(ClientState client,
+            @NonNull UserDataRepository.UserData userData) {
+        final int userId = userData.mUserId;
+        if (userData.mCurClient == client) {
+            hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */,
+                    SoftInputShowHideReason.HIDE_REMOVE_CLIENT, userId);
+            if (userData.mBoundToMethod) {
+                userData.mBoundToMethod = false;
+                final var userBindingController = getInputMethodBindingController(userId);
+                IInputMethodInvoker curMethod = userBindingController.getCurMethod();
                 if (curMethod != null) {
                     // When we unbind input, we are unbinding the client, so we always
                     // unbind ime and a11y together.
@@ -1880,10 +1795,10 @@
                     AccessibilityManagerInternal.get().unbindInput();
                 }
             }
-            mBoundToAccessibility = false;
-            mCurClient = null;
-            if (mImeBindingState.mFocusedWindowClient == client) {
-                mImeBindingState = ImeBindingState.newEmptyState();
+            userData.mBoundToAccessibility = false;
+            userData.mCurClient = null;
+            if (userData.mImeBindingState.mFocusedWindowClient == client) {
+                userData.mImeBindingState = ImeBindingState.newEmptyState();
             }
         }
     }
@@ -1896,49 +1811,51 @@
     }
 
     @GuardedBy("ImfLock.class")
-    void unbindCurrentClientLocked(@UnbindReason int unbindClientReason) {
-        if (mCurClient != null) {
+    void unbindCurrentClientLocked(@UnbindReason int unbindClientReason, @UserIdInt int userId) {
+        final var userData = getUserData(userId);
+        if (userData.mCurClient != null) {
             if (DEBUG) {
-                Slog.v(TAG, "unbindCurrentInputLocked: client=" + mCurClient.mClient.asBinder());
+                Slog.v(TAG, "unbindCurrentInputLocked: client="
+                        + userData.mCurClient.mClient.asBinder());
             }
-            if (mBoundToMethod) {
-                mBoundToMethod = false;
-                IInputMethodInvoker curMethod = getCurMethodLocked();
+            final var bindingController = getInputMethodBindingController(userId);
+            if (userData.mBoundToMethod) {
+                userData.mBoundToMethod = false;
+                IInputMethodInvoker curMethod = bindingController.getCurMethod();
                 if (curMethod != null) {
                     curMethod.unbindInput();
                 }
             }
-            mBoundToAccessibility = false;
+            userData.mBoundToAccessibility = false;
 
             // Since we set active false to current client and set mCurClient to null, let's unbind
             // all accessibility too. That means, when input method get disconnected (including
             // switching ime), we also unbind accessibility
-            mCurClient.mClient.setActive(false /* active */, false /* fullscreen */);
+            userData.mCurClient.mClient.setActive(false /* active */, false /* fullscreen */);
 
-            // TODO(b/325515685): make binding controller user independent. Before this change, the
-            //  following dependencies also need to be user independent: mCurClient, mBoundToMethod,
-            //  getCurMethodLocked(), and mMenuController.
-            final var bindingController = getInputMethodBindingController(mCurrentUserId);
-            mCurClient.mClient.onUnbindMethod(bindingController.getSequenceNumber(),
+            userData.mCurClient.mClient.onUnbindMethod(bindingController.getSequenceNumber(),
                     unbindClientReason);
-            mCurClient.mSessionRequested = false;
-            mCurClient.mSessionRequestedForAccessibility = false;
-            mCurClient = null;
-            ImeTracker.forLogging().onFailed(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
-            mCurStatsToken = null;
+            userData.mCurClient.mSessionRequested = false;
+            userData.mCurClient.mSessionRequestedForAccessibility = false;
+            userData.mCurClient = null;
+            ImeTracker.forLogging().onFailed(userData.mCurStatsToken,
+                    ImeTracker.PHASE_SERVER_WAIT_IME);
+            userData.mCurStatsToken = null;
+            // TODO: Make mMenuController multi-user aware
             mMenuController.hideInputMethodMenuLocked();
         }
     }
 
     /**
      * TODO(b/338404383) Remove
-     * Called when {@link #resetCurrentMethodAndClientLocked(int)} invoked for clean-up states
+     * Called when {@link #resetCurrentMethodAndClientLocked(int, int)} invoked for clean-up states
      * before unbinding the current method.
      */
     @GuardedBy("ImfLock.class")
-    void onUnbindCurrentMethodByReset() {
+    void onUnbindCurrentMethodByReset(@UserIdInt int userId) {
+        final var userData = getUserData(userId);
         final ImeTargetWindowState winState = mVisibilityStateComputer.getWindowStateOrNull(
-                mImeBindingState.mFocusedWindow);
+                userData.mImeBindingState.mFocusedWindow);
         if (winState != null && !winState.isRequestedImeVisible()
                 && !mVisibilityStateComputer.isInputShown()) {
             // Normally, the focus window will apply the IME visibility state to
@@ -1949,9 +1866,10 @@
             // As a result, we have to notify WM to apply IME visibility before clearing the
             // binding states in the first place.
             final var statsToken = createStatsTokenForFocusedClient(false /* show */,
-                    SoftInputShowHideReason.UNBIND_CURRENT_METHOD);
-            mVisibilityApplier.applyImeVisibility(mImeBindingState.mFocusedWindow, statsToken,
-                    STATE_HIDE_IME, mCurrentUserId);
+                    SoftInputShowHideReason.UNBIND_CURRENT_METHOD, userId);
+            mVisibilityApplier.applyImeVisibility(userData.mImeBindingState.mFocusedWindow,
+                    statsToken, STATE_HIDE_IME, SoftInputShowHideReason.NOT_SET /* ignore reason */,
+                    userId);
         }
     }
 
@@ -1961,13 +1879,13 @@
      */
     @GuardedBy("ImfLock.class")
     boolean hasAttachedClient() {
-        return mCurClient != null;
+        return getUserData(mCurrentUserId).mCurClient != null;
     }
 
     @VisibleForTesting
     void setAttachedClientForTesting(@NonNull ClientState cs) {
         synchronized (ImfLock.class) {
-            mCurClient = cs;
+            getUserData(mCurrentUserId).mCurClient = cs;
         }
     }
 
@@ -1983,20 +1901,23 @@
     }
 
     @GuardedBy("ImfLock.class")
-    private boolean isShowRequestedForCurrentWindow() {
+    private boolean isShowRequestedForCurrentWindow(@UserIdInt int userId) {
+        final var userData = getUserData(userId);
+        // TODO(b/349904272): Make mVisibilityStateComputer multi-user aware
         final ImeTargetWindowState state = mVisibilityStateComputer.getWindowStateOrNull(
-                mImeBindingState.mFocusedWindow);
+                userData.mImeBindingState.mFocusedWindow);
         return state != null && state.isRequestedImeVisible();
     }
 
     @GuardedBy("ImfLock.class")
     @NonNull
-    InputBindResult attachNewInputLocked(@StartInputReason int startInputReason, boolean initial) {
-        final int userId = mCurrentUserId;
+    InputBindResult attachNewInputLocked(@StartInputReason int startInputReason, boolean initial,
+            @UserIdInt int userId) {
         final var bindingController = getInputMethodBindingController(userId);
-        if (!mBoundToMethod) {
-            bindingController.getCurMethod().bindInput(mCurClient.mBinding);
-            mBoundToMethod = true;
+        final var userData = getUserData(userId);
+        if (!userData.mBoundToMethod) {
+            bindingController.getCurMethod().bindInput(userData.mCurClient.mBinding);
+            userData.mBoundToMethod = true;
         }
 
         final boolean restarting = !initial;
@@ -2004,11 +1925,12 @@
         final StartInputInfo info = new StartInputInfo(mCurrentUserId,
                 bindingController.getCurToken(), bindingController.getCurTokenDisplayId(),
                 bindingController.getCurId(), startInputReason,
-                restarting, UserHandle.getUserId(mCurClient.mUid),
-                mCurClient.mSelfReportedDisplayId, mImeBindingState.mFocusedWindow, mCurEditorInfo,
-                mImeBindingState.mFocusedWindowSoftInputMode,
+                restarting, UserHandle.getUserId(userData.mCurClient.mUid),
+                userData.mCurClient.mSelfReportedDisplayId,
+                userData.mImeBindingState.mFocusedWindow, userData.mCurEditorInfo,
+                userData.mImeBindingState.mFocusedWindowSoftInputMode,
                 bindingController.getSequenceNumber());
-        mImeTargetWindowMap.put(startInputToken, mImeBindingState.mFocusedWindow);
+        mImeTargetWindowMap.put(startInputToken, userData.mImeBindingState.mFocusedWindow);
         mStartInputHistory.addEntry(info);
 
         // Seems that PackageManagerInternal#grantImplicitAccess() doesn't handle cross-user
@@ -2016,33 +1938,34 @@
         // same-user scenarios.
         // That said ignoring cross-user scenario will never affect IMEs that do not have
         // INTERACT_ACROSS_USERS(_FULL) permissions, which is actually almost always the case.
-        if (userId == UserHandle.getUserId(mCurClient.mUid)) {
+        if (userId == UserHandle.getUserId(userData.mCurClient.mUid)) {
             mPackageManagerInternal.grantImplicitAccess(userId, null /* intent */,
                     UserHandle.getAppId(bindingController.getCurMethodUid()),
-                    mCurClient.mUid, true /* direct */);
+                    userData.mCurClient.mUid, true /* direct */);
         }
 
         @InputMethodNavButtonFlags final int navButtonFlags = getInputMethodNavButtonFlagsLocked();
-        final SessionState session = mCurClient.mCurSession;
-        setEnabledSessionLocked(session);
-        session.mMethod.startInput(startInputToken, mCurInputConnection, mCurEditorInfo, restarting,
-                navButtonFlags, mCurImeDispatcher);
+        final SessionState session = userData.mCurClient.mCurSession;
+        setEnabledSessionLocked(session, userData);
+        session.mMethod.startInput(startInputToken, userData.mCurInputConnection,
+                userData.mCurEditorInfo, restarting, navButtonFlags, userData.mCurImeDispatcher);
         if (Flags.refactorInsetsController()) {
-            if (isShowRequestedForCurrentWindow() && mImeBindingState != null
-                    && mImeBindingState.mFocusedWindow != null) {
-                showSoftInputInternal(mImeBindingState.mFocusedWindow);
+            if (isShowRequestedForCurrentWindow(userId) && userData.mImeBindingState != null
+                    && userData.mImeBindingState.mFocusedWindow != null) {
+                showSoftInputInternal(userData.mImeBindingState.mFocusedWindow);
             }
         } else {
-            if (isShowRequestedForCurrentWindow()) {
+            if (isShowRequestedForCurrentWindow(userId)) {
                 if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");
                 // Re-use current statsToken, if it exists.
-                final var statsToken = mCurStatsToken != null ? mCurStatsToken
+                final var statsToken = userData.mCurStatsToken != null ? userData.mCurStatsToken
                     : createStatsTokenForFocusedClient(true /* show */,
-                            SoftInputShowHideReason.ATTACH_NEW_INPUT);
-                mCurStatsToken = null;
-                showCurrentInputLocked(mImeBindingState.mFocusedWindow, statsToken,
+                            SoftInputShowHideReason.ATTACH_NEW_INPUT, userId);
+                userData.mCurStatsToken = null;
+                showCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, statsToken,
                         mVisibilityStateComputer.getShowFlags(), MotionEvent.TOOL_TYPE_UNKNOWN,
-                        null /* resultReceiver */, SoftInputShowHideReason.ATTACH_NEW_INPUT);
+                        null /* resultReceiver */, SoftInputShowHideReason.ATTACH_NEW_INPUT,
+                        userId);
             }
         }
 
@@ -2052,7 +1975,8 @@
         final boolean suppressesSpellChecker =
                 curInputMethodInfo != null && curInputMethodInfo.suppressesSpellChecker();
         final SparseArray<IAccessibilityInputMethodSession> accessibilityInputMethodSessions =
-                createAccessibilityInputMethodSessions(mCurClient.mAccessibilitySessions);
+                createAccessibilityInputMethodSessions(
+                        userData.mCurClient.mAccessibilitySessions);
         if (bindingController.supportsStylusHandwriting() && hasSupportedStylusLocked()) {
             mHwController.setInkWindowInitializer(new InkWindowInitializer());
         }
@@ -2064,10 +1988,12 @@
 
     @GuardedBy("ImfLock.class")
     private void attachNewAccessibilityLocked(@StartInputReason int startInputReason,
-            boolean initial) {
-        if (!mBoundToAccessibility) {
+            boolean initial, @UserIdInt int userId) {
+        final var userData = getUserData(userId);
+
+        if (!userData.mBoundToAccessibility) {
             AccessibilityManagerInternal.get().bindInput();
-            mBoundToAccessibility = true;
+            userData.mBoundToAccessibility = true;
         }
 
         // TODO(b/187453053): grantImplicitAccess to accessibility services access? if so, need to
@@ -2076,9 +2002,11 @@
         // We don't start input when session for a11y is created. We start input when
         // input method start input, a11y manager service is always on.
         if (startInputReason != StartInputReason.SESSION_CREATED_BY_ACCESSIBILITY) {
-            setEnabledSessionForAccessibilityLocked(mCurClient.mAccessibilitySessions);
-            AccessibilityManagerInternal.get().startInput(mCurRemoteAccessibilityInputConnection,
-                    mCurEditorInfo, !initial /* restarting */);
+            setEnabledSessionForAccessibilityLocked(userData.mCurClient.mAccessibilitySessions,
+                    userData);
+            AccessibilityManagerInternal.get().startInput(
+                    userData.mCurRemoteAccessibilityInputConnection,
+                    userData.mCurEditorInfo, !initial /* restarting */);
         }
     }
 
@@ -2114,10 +2042,13 @@
             @NonNull ImeOnBackInvokedDispatcher imeDispatcher,
             @NonNull InputMethodBindingController bindingController) {
 
+        final int userId = bindingController.getUserId();
+        final var userData = getUserData(userId);
+
         // Compute the final shown display ID with validated cs.selfReportedDisplayId for this
         // session & other conditions.
         ImeTargetWindowState winState = mVisibilityStateComputer.getWindowStateOrNull(
-                mImeBindingState.mFocusedWindow);
+                userData.mImeBindingState.mFocusedWindow);
         if (winState == null) {
             return InputBindResult.NOT_IME_TARGET_WINDOW;
         }
@@ -2128,19 +2059,19 @@
         // Potentially override the selected input method if the new display belongs to a virtual
         // device with a custom IME.
         String selectedMethodId = bindingController.getSelectedMethodId();
-        final String deviceMethodId = computeCurrentDeviceMethodIdLocked(bindingController.mUserId,
-                selectedMethodId);
+        final String deviceMethodId = computeCurrentDeviceMethodIdLocked(
+                bindingController.getUserId(), selectedMethodId);
         if (deviceMethodId == null) {
             mVisibilityStateComputer.getImePolicy().setImeHiddenByDisplayPolicy(true);
         } else if (!Objects.equals(deviceMethodId, selectedMethodId)) {
             setInputMethodLocked(deviceMethodId, NOT_A_SUBTYPE_ID,
-                    bindingController.getDeviceIdToShowIme());
+                    bindingController.getDeviceIdToShowIme(), userId);
             selectedMethodId = deviceMethodId;
         }
 
         if (mVisibilityStateComputer.getImePolicy().isImeHiddenByDisplayPolicy()) {
-            hideCurrentInputLocked(mImeBindingState.mFocusedWindow, 0 /* flags */,
-                    SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE);
+            hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */,
+                    SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE, userId);
             return InputBindResult.NO_IME;
         }
 
@@ -2149,19 +2080,19 @@
             return InputBindResult.NO_IME;
         }
 
-        if (mCurClient != cs) {
-            prepareClientSwitchLocked(cs);
+        if (userData.mCurClient != cs) {
+            prepareClientSwitchLocked(cs, userId);
         }
 
-        final boolean connectionWasActive = mCurInputConnection != null;
+        final boolean connectionWasActive = userData.mCurInputConnection != null;
 
         // Bump up the sequence for this client and attach it.
         bindingController.advanceSequenceNumber();
 
-        mCurClient = cs;
-        mCurInputConnection = inputConnection;
-        mCurRemoteAccessibilityInputConnection = remoteAccessibilityInputConnection;
-        mCurImeDispatcher = imeDispatcher;
+        userData.mCurClient = cs;
+        userData.mCurInputConnection = inputConnection;
+        userData.mCurRemoteAccessibilityInputConnection = remoteAccessibilityInputConnection;
+        userData.mCurImeDispatcher = imeDispatcher;
         // Override the locale hints if the app is running on a virtual device.
         if (mVdmInternal == null) {
             mVdmInternal = LocalServices.getService(VirtualDeviceManagerInternal.class);
@@ -2172,17 +2103,17 @@
                 editorInfo.hintLocales = hintsFromVirtualDevice;
             }
         }
-        mCurEditorInfo = editorInfo;
+        userData.mCurEditorInfo = editorInfo;
 
         // Notify input manager if the connection state changes.
-        final boolean connectionIsActive = mCurInputConnection != null;
+        final boolean connectionIsActive = userData.mCurInputConnection != null;
         if (connectionIsActive != connectionWasActive) {
             mInputManagerInternal.notifyInputMethodConnectionActive(connectionIsActive);
         }
 
         // If configured, we want to avoid starting up the IME if it is not supposed to be showing
         if (shouldPreventImeStartupLocked(selectedMethodId, startInputFlags,
-                unverifiedTargetSdkVersion)) {
+                unverifiedTargetSdkVersion, userId)) {
             if (DEBUG) {
                 Slog.d(TAG, "Avoiding IME startup and unbinding current input method.");
             }
@@ -2197,7 +2128,7 @@
         final String curId = bindingController.getCurId();
         final int displayIdToShowIme = bindingController.getDisplayIdToShowIme();
         if (curId != null && curId.equals(bindingController.getSelectedMethodId())
-                && displayIdToShowIme == getCurTokenDisplayIdLocked()) {
+                && displayIdToShowIme == bindingController.getCurTokenDisplayId()) {
             if (cs.mCurSession != null) {
                 // Fast case: if we are already connected to the input method,
                 // then just return it.
@@ -2211,12 +2142,12 @@
                 // we can always attach to accessibility because AccessibilityManagerService is
                 // always on.
                 attachNewAccessibilityLocked(startInputReason,
-                        (startInputFlags & StartInputFlags.INITIAL_CONNECTION) != 0);
+                        (startInputFlags & StartInputFlags.INITIAL_CONNECTION) != 0, userId);
                 return attachNewInputLocked(startInputReason,
-                        (startInputFlags & StartInputFlags.INITIAL_CONNECTION) != 0);
+                        (startInputFlags & StartInputFlags.INITIAL_CONNECTION) != 0, userId);
             }
 
-            InputBindResult bindResult = tryReuseConnectionLocked(bindingController, cs);
+            InputBindResult bindResult = tryReuseConnectionLocked(bindingController, cs, userId);
             if (bindResult != null) {
                 return bindResult;
             }
@@ -2293,18 +2224,19 @@
     private boolean shouldPreventImeStartupLocked(
             @NonNull String selectedMethodId,
             @StartInputFlags int startInputFlags,
-            int unverifiedTargetSdkVersion) {
+            int unverifiedTargetSdkVersion,
+            @UserIdInt int userId) {
         // Fast-path for the majority of cases
         if (!mPreventImeStartupUnlessTextEditor) {
             return false;
         }
-        if (isShowRequestedForCurrentWindow()) {
+        if (isShowRequestedForCurrentWindow(userId)) {
             return false;
         }
         if (isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags)) {
             return false;
         }
-        final InputMethodInfo imi = InputMethodSettingsRepository.get(mCurrentUserId)
+        final InputMethodInfo imi = InputMethodSettingsRepository.get(userId)
                 .getMethodMap().get(selectedMethodId);
         if (imi == null) {
             return false;
@@ -2316,10 +2248,10 @@
     }
 
     @GuardedBy("ImfLock.class")
-    private void prepareClientSwitchLocked(ClientState cs) {
+    private void prepareClientSwitchLocked(ClientState cs, @UserIdInt int userId) {
         // If the client is changing, we need to switch over to the new
         // one.
-        unbindCurrentClientLocked(UnbindReason.SWITCH_CLIENT);
+        unbindCurrentClientLocked(UnbindReason.SWITCH_CLIENT, userId);
         // If the screen is on, inform the new client it is active
         if (mIsInteractive) {
             cs.mClient.setActive(true /* active */, false /* fullscreen */);
@@ -2329,13 +2261,14 @@
     @GuardedBy("ImfLock.class")
     @Nullable
     private InputBindResult tryReuseConnectionLocked(
-            @NonNull InputMethodBindingController bindingController, @NonNull ClientState cs) {
+            @NonNull InputMethodBindingController bindingController, @NonNull ClientState cs,
+            @UserIdInt int userId) {
         if (bindingController.hasMainConnection()) {
-            if (getCurMethodLocked() != null) {
+            if (bindingController.getCurMethod() != null) {
                 if (!Flags.useZeroJankProxy()) {
                     // Return to client, and we will get back with it when
                     // we have had a session made for it.
-                    requestClientSessionLocked(cs);
+                    requestClientSessionLocked(cs, userId);
                     requestClientSessionForAccessibilityLocked(cs);
                 }
                 return new InputBindResult(
@@ -2361,7 +2294,7 @@
                             bindingController.getSequenceNumber(), false);
                 } else {
                     EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME,
-                            getSelectedMethodIdLocked(), bindingDuration, 0);
+                            bindingController.getSelectedMethodId(), bindingDuration, 0);
                 }
             }
         }
@@ -2402,12 +2335,15 @@
     }
 
     @GuardedBy("ImfLock.class")
-    void initializeImeLocked(@NonNull IInputMethodInvoker inputMethod, @NonNull IBinder token) {
+    void initializeImeLocked(@NonNull IInputMethodInvoker inputMethod, @NonNull IBinder token,
+            @UserIdInt int userId) {
         if (DEBUG) {
             Slog.v(TAG, "Sending attach of token: " + token + " for display: "
-                    + getCurTokenDisplayIdLocked());
+                    + getInputMethodBindingController(userId).getCurTokenDisplayId());
         }
-        inputMethod.initializeInternal(token, new InputMethodPrivilegedOperationsImpl(this, token),
+        inputMethod.initializeInternal(token,
+                new InputMethodPrivilegedOperationsImpl(this, token, userId),
+                // TODO(b/345519864): Make getInputMethodNavButtonFlagsLocked() multi-user aware
                 getInputMethodNavButtonFlagsLocked());
     }
 
@@ -2438,7 +2374,7 @@
 
     @BinderThread
     void onSessionCreated(IInputMethodInvoker method, IInputMethodSession session,
-            InputChannel channel) {
+            InputChannel channel, @UserIdInt int userId) {
         Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.onSessionCreated");
         try {
             synchronized (ImfLock.class) {
@@ -2448,18 +2384,21 @@
                     channel.dispose();
                     return;
                 }
-                IInputMethodInvoker curMethod = getCurMethodLocked();
+                final var bindingController = getInputMethodBindingController(userId);
+                final var userData = getUserData(userId);
+                IInputMethodInvoker curMethod = bindingController.getCurMethod();
                 if (curMethod != null && method != null
                         && curMethod.asBinder() == method.asBinder()) {
-                    if (mCurClient != null) {
-                        clearClientSessionLocked(mCurClient);
-                        mCurClient.mCurSession = new SessionState(
-                                mCurClient, method, session, channel);
+                    if (userData.mCurClient != null) {
+                        clearClientSessionLocked(userData.mCurClient);
+                        userData.mCurClient.mCurSession = new SessionState(
+                                userData.mCurClient, method, session, channel);
                         InputBindResult res = attachNewInputLocked(
-                                StartInputReason.SESSION_CREATED_BY_IME, true);
-                        attachNewAccessibilityLocked(StartInputReason.SESSION_CREATED_BY_IME, true);
+                                StartInputReason.SESSION_CREATED_BY_IME, true, userId);
+                        attachNewAccessibilityLocked(StartInputReason.SESSION_CREATED_BY_IME, true,
+                                userId);
                         if (res.method != null) {
-                            mCurClient.mClient.onBindMethod(res);
+                            userData.mCurClient.mClient.onBindMethod(res);
                         }
                         return;
                     }
@@ -2482,29 +2421,31 @@
     }
 
     @GuardedBy("ImfLock.class")
-    void resetCurrentMethodAndClientLocked(@UnbindReason int unbindClientReason) {
-        final var bindingController = getInputMethodBindingController(mCurrentUserId);
+    void resetCurrentMethodAndClientLocked(@UnbindReason int unbindClientReason,
+            @UserIdInt int userId) {
+        final var bindingController = getInputMethodBindingController(userId);
         bindingController.setSelectedMethodId(null);
 
         // Callback before clean-up binding states.
         // TODO(b/338461930): Check if this is still necessary or not.
-        onUnbindCurrentMethodByReset();
+        onUnbindCurrentMethodByReset(userId);
         bindingController.unbindCurrentMethod();
-        unbindCurrentClientLocked(unbindClientReason);
+        unbindCurrentClientLocked(unbindClientReason, userId);
     }
 
     @GuardedBy("ImfLock.class")
-    void reRequestCurrentClientSessionLocked() {
-        if (mCurClient != null) {
-            clearClientSessionLocked(mCurClient);
-            clearClientSessionForAccessibilityLocked(mCurClient);
-            requestClientSessionLocked(mCurClient);
-            requestClientSessionForAccessibilityLocked(mCurClient);
+    void reRequestCurrentClientSessionLocked(@UserIdInt int userId) {
+        final var userData = getUserData(userId);
+        if (userData.mCurClient != null) {
+            clearClientSessionLocked(userData.mCurClient);
+            clearClientSessionForAccessibilityLocked(userData.mCurClient);
+            requestClientSessionLocked(userData.mCurClient, userId);
+            requestClientSessionForAccessibilityLocked(userData.mCurClient);
         }
     }
 
     @GuardedBy("ImfLock.class")
-    void requestClientSessionLocked(ClientState cs) {
+    void requestClientSessionLocked(ClientState cs, @UserIdInt int userId) {
         if (!cs.mSessionRequested) {
             if (DEBUG) Slog.v(TAG, "Creating new session for client " + cs);
             final InputChannel serverChannel;
@@ -2517,14 +2458,15 @@
 
             cs.mSessionRequested = true;
 
-            final IInputMethodInvoker curMethod = getCurMethodLocked();
+            final var bindingController = getInputMethodBindingController(userId);
+            final IInputMethodInvoker curMethod = bindingController.getCurMethod();
             final IInputMethodSessionCallback.Stub callback =
                     new IInputMethodSessionCallback.Stub() {
                         @Override
                         public void sessionCreated(IInputMethodSession session) {
                             final long ident = Binder.clearCallingIdentity();
                             try {
-                                onSessionCreated(curMethod, session, serverChannel);
+                                onSessionCreated(curMethod, session, serverChannel, userId);
                             } finally {
                                 Binder.restoreCallingIdentity(ident);
                             }
@@ -2615,33 +2557,42 @@
     }
 
     @GuardedBy("ImfLock.class")
-    void clearClientSessionsLocked() {
-        if (getCurMethodLocked() != null) {
+    void clearClientSessionsLocked(@NonNull InputMethodBindingController bindingController) {
+        final int userId = bindingController.getUserId();
+        final var userData = getUserData(userId);
+        if (bindingController.getCurMethod() != null) {
             // TODO(b/324907325): Remove the suppress warnings once b/324907325 is fixed.
             @SuppressWarnings("GuardedBy") Consumer<ClientState> clearClientSession = c -> {
-                clearClientSessionLocked(c);
-                clearClientSessionForAccessibilityLocked(c);
+                // TODO(b/305849394): Figure out what we should do for single user IME mode.
+                final boolean shouldClearClientSession =
+                        !mExperimentalConcurrentMultiUserModeEnabled
+                                || UserHandle.getUserId(c.mUid) == userId;
+                if (shouldClearClientSession) {
+                    clearClientSessionLocked(c);
+                    clearClientSessionForAccessibilityLocked(c);
+                }
             };
             mClientController.forAllClients(clearClientSession);
 
-            finishSessionLocked(mEnabledSession);
-            for (int i = 0; i < mEnabledAccessibilitySessions.size(); i++) {
-                finishSessionForAccessibilityLocked(mEnabledAccessibilitySessions.valueAt(i));
+            finishSessionLocked(userData.mEnabledSession);
+            for (int i = 0; i < userData.mEnabledAccessibilitySessions.size(); i++) {
+                finishSessionForAccessibilityLocked(
+                        userData.mEnabledAccessibilitySessions.valueAt(i));
             }
-            mEnabledSession = null;
-            mEnabledAccessibilitySessions.clear();
+            userData.mEnabledSession = null;
+            userData.mEnabledAccessibilitySessions.clear();
             scheduleNotifyImeUidToAudioService(Process.INVALID_UID);
         }
         hideStatusBarIconLocked();
-        mInFullscreenMode = false;
+        getUserData(userId).mInFullscreenMode = false;
         mWindowManagerInternal.setDismissImeOnBackKeyPressed(false);
     }
 
     @BinderThread
     private void updateStatusIcon(@NonNull IBinder token, String packageName,
-            @DrawableRes int iconId) {
+            @DrawableRes int iconId, @UserIdInt int userId) {
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token)) {
+            if (!calledWithValidTokenLocked(token, userId)) {
                 return;
             }
             final long ident = Binder.clearCallingIdentity();
@@ -2783,24 +2734,26 @@
 
     @BinderThread
     @SuppressWarnings("deprecation")
-    private void setImeWindowStatus(@NonNull IBinder token, int vis, int backDisposition) {
+    private void setImeWindowStatus(@NonNull IBinder token, int vis, int backDisposition,
+            @UserIdInt int userId) {
         final int topFocusedDisplayId = mWindowManagerInternal.getTopFocusedDisplayId();
 
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token)) {
+            if (!calledWithValidTokenLocked(token, userId)) {
                 return;
             }
+            final var bindingController = getInputMethodBindingController(userId);
             // Skip update IME status when current token display is not same as focused display.
             // Note that we still need to update IME status when focusing external display
             // that does not support system decoration and fallback to show IME on default
             // display since it is intentional behavior.
-            final int tokenDisplayId = getCurTokenDisplayIdLocked();
+            final int tokenDisplayId = bindingController.getCurTokenDisplayId();
             if (tokenDisplayId != topFocusedDisplayId && tokenDisplayId != FALLBACK_DISPLAY_ID) {
                 return;
             }
             mImeWindowVis = vis;
             mBackDisposition = backDisposition;
-            updateSystemUiLocked(vis, backDisposition);
+            updateSystemUiLocked(vis, backDisposition, userId);
         }
 
         final boolean dismissImeOnBackKeyPressed;
@@ -2820,9 +2773,10 @@
     }
 
     @BinderThread
-    private void reportStartInput(@NonNull IBinder token, IBinder startInputToken) {
+    private void reportStartInput(@NonNull IBinder token, IBinder startInputToken,
+            @UserIdInt int userId) {
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token)) {
+            if (!calledWithValidTokenLocked(token, userId)) {
                 return;
             }
             final IBinder targetWindow = mImeTargetWindowMap.get(startInputToken);
@@ -2857,6 +2811,7 @@
     @GuardedBy("ImfLock.class")
     private void updateSystemUiLocked(int vis, int backDisposition, @UserIdInt int userId) {
         final var bindingController = getInputMethodBindingController(userId);
+        final var userData = getUserData(userId);
         final var curToken = bindingController.getCurToken();
         if (curToken == null) {
             return;
@@ -2868,8 +2823,8 @@
                     + " inv: " + (vis & InputMethodService.IME_INVISIBLE)
                     + " displayId: " + curTokenDisplayId);
         }
-        final IBinder focusedWindowToken = mImeBindingState != null
-                ? mImeBindingState.mFocusedWindow : null;
+        final IBinder focusedWindowToken = userData.mImeBindingState != null
+                ? userData.mImeBindingState.mFocusedWindow : null;
         final Boolean windowPerceptible = focusedWindowToken != null
                 ? mFocusedWindowPerceptible.get(focusedWindowToken) : null;
 
@@ -2904,8 +2859,8 @@
     }
 
     @GuardedBy("ImfLock.class")
-    void updateFromSettingsLocked(boolean enabledMayChange) {
-        updateInputMethodsFromSettingsLocked(enabledMayChange);
+    void updateFromSettingsLocked(boolean enabledMayChange, @UserIdInt int userId) {
+        updateInputMethodsFromSettingsLocked(enabledMayChange, userId);
         mMenuController.updateKeyboardFromSettingsLocked();
     }
 
@@ -2915,7 +2870,7 @@
      *
      * <p>Never assume what this method is doing is officially supported. For the canonical and
      * desired behaviors always refer to single-user code paths such as
-     * {@link #updateInputMethodsFromSettingsLocked(boolean)}.</p>
+     * {@link #updateInputMethodsFromSettingsLocked(boolean, int)}.</p>
      *
      * <p>Here are examples of missing features.</p>
      * <ul>
@@ -2964,8 +2919,7 @@
     }
 
     @GuardedBy("ImfLock.class")
-    void updateInputMethodsFromSettingsLocked(boolean enabledMayChange) {
-        final int userId = mCurrentUserId;
+    void updateInputMethodsFromSettingsLocked(boolean enabledMayChange, @UserIdInt int userId) {
         final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
         if (enabledMayChange) {
             final PackageManager userAwarePackageManager = getPackageManagerForUser(mContext,
@@ -2996,7 +2950,7 @@
             }
         }
 
-        final var bindingController = getInputMethodBindingController(mCurrentUserId);
+        final var bindingController = getInputMethodBindingController(userId);
         if (bindingController.getDeviceIdToShowIme() == DEVICE_ID_DEFAULT) {
             String ime = SecureSettingsWrapper.getString(
                     Settings.Secure.DEFAULT_INPUT_METHOD, null, userId);
@@ -3026,14 +2980,14 @@
         }
         if (!TextUtils.isEmpty(id)) {
             try {
-                setInputMethodLocked(id, settings.getSelectedInputMethodSubtypeId(id));
+                setInputMethodLocked(id, settings.getSelectedInputMethodSubtypeId(id), userId);
             } catch (IllegalArgumentException e) {
                 Slog.w(TAG, "Unknown input method from prefs: " + id, e);
-                resetCurrentMethodAndClientLocked(UnbindReason.SWITCH_IME_FAILED);
+                resetCurrentMethodAndClientLocked(UnbindReason.SWITCH_IME_FAILED, userId);
             }
         } else {
             // There is no longer an input method set, so stop any current one.
-            resetCurrentMethodAndClientLocked(UnbindReason.NO_IME);
+            resetCurrentMethodAndClientLocked(UnbindReason.NO_IME, userId);
         }
 
         final var userData = getUserData(userId);
@@ -3055,13 +3009,12 @@
     }
 
     @GuardedBy("ImfLock.class")
-    void setInputMethodLocked(String id, int subtypeId) {
-        setInputMethodLocked(id, subtypeId, DEVICE_ID_DEFAULT);
+    void setInputMethodLocked(String id, int subtypeId, @UserIdInt int userId) {
+        setInputMethodLocked(id, subtypeId, DEVICE_ID_DEFAULT, userId);
     }
 
     @GuardedBy("ImfLock.class")
-    void setInputMethodLocked(String id, int subtypeId, int deviceId) {
-        final int userId = mCurrentUserId;
+    void setInputMethodLocked(String id, int subtypeId, int deviceId, @UserIdInt int userId) {
         final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
         InputMethodInfo info = settings.getMethodMap().get(id);
         if (info == null) {
@@ -3096,8 +3049,8 @@
                 }
             }
             if (!Objects.equals(newSubtype, oldSubtype)) {
-                setSelectedInputMethodAndSubtypeLocked(info, subtypeId, true);
-                IInputMethodInvoker curMethod = getCurMethodLocked();
+                setSelectedInputMethodAndSubtypeLocked(info, subtypeId, true, userId);
+                IInputMethodInvoker curMethod = bindingController.getCurMethod();
                 if (curMethod != null) {
                     updateSystemUiLocked(mImeWindowVis, mBackDisposition);
                     curMethod.changeInputMethodSubtype(newSubtype);
@@ -3116,7 +3069,7 @@
             settings.putSelectedDefaultDeviceInputMethod(id);
             return;
         }
-        IInputMethodInvoker curMethod = getCurMethodLocked();
+        IInputMethodInvoker curMethod = bindingController.getCurMethod();
         if (curMethod != null) {
             curMethod.removeStylusHandwritingWindow();
         }
@@ -3124,7 +3077,7 @@
         try {
             // Set a subtype to this input method.
             // subtypeId the name of a subtype which will be set.
-            setSelectedInputMethodAndSubtypeLocked(info, subtypeId, false);
+            setSelectedInputMethodAndSubtypeLocked(info, subtypeId, false, userId);
             // mCurMethodId should be updated after setSelectedInputMethodAndSubtypeLocked()
             // because mCurMethodId is stored as a history in
             // setSelectedInputMethodAndSubtypeLocked().
@@ -3136,7 +3089,7 @@
                 intent.putExtra("input_method_id", id);
                 mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
             }
-            unbindCurrentClientLocked(UnbindReason.SWITCH_IME);
+            unbindCurrentClientLocked(UnbindReason.SWITCH_IME, userId);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -3158,14 +3111,20 @@
                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
                 return false;
             }
+            // TODO(b/305849394): Create a utility method for the following policy.
+            final int userId = mExperimentalConcurrentMultiUserModeEnabled
+                    ? UserHandle.getCallingUserId() : mCurrentUserId;
             final long ident = Binder.clearCallingIdentity();
+            final var userData = getUserData(userId);
             try {
                 if (DEBUG) Slog.v(TAG, "Client requesting input be shown");
                 if (Flags.refactorInsetsController()) {
                     boolean wasVisible = isInputShownLocked();
-                    if (mImeBindingState != null && mImeBindingState.mFocusedWindowClient != null
-                            && mImeBindingState.mFocusedWindowClient.mClient != null) {
-                        mImeBindingState.mFocusedWindowClient.mClient.setImeVisibility(true);
+                    if (userData.mImeBindingState != null
+                            && userData.mImeBindingState.mFocusedWindowClient != null
+                            && userData.mImeBindingState.mFocusedWindowClient.mClient != null) {
+                        userData.mImeBindingState.mFocusedWindowClient.mClient
+                                .setImeVisibility(true);
                         if (resultReceiver != null) {
                             resultReceiver.send(
                                     wasVisible ? InputMethodManager.RESULT_UNCHANGED_SHOWN
@@ -3176,7 +3135,7 @@
                     return false;
                 } else {
                     return showCurrentInputLocked(windowToken, statsToken, flags, lastClickToolType,
-                            resultReceiver, reason);
+                            resultReceiver, reason, userId);
                 }
             } finally {
                 Binder.restoreCallingIdentity(ident);
@@ -3190,12 +3149,14 @@
         ImeTracing.getInstance().triggerManagerServiceDump(
                 "InputMethodManagerService#showSoftInput", mDumper);
         synchronized (ImfLock.class) {
+            // TODO(b/305849394): Infer userId from windowToken
+            final int userId = mCurrentUserId;
             final long ident = Binder.clearCallingIdentity();
             try {
                 if (DEBUG) Slog.v(TAG, "Client requesting input be shown");
                 return showCurrentInputLocked(windowToken, null /* statsToken */, 0 /* flags */,
                         0 /* lastClickTooType */, null /* resultReceiver */,
-                        SoftInputShowHideReason.SHOW_SOFT_INPUT);
+                        SoftInputShowHideReason.SHOW_SOFT_INPUT, userId);
             } finally {
                 Binder.restoreCallingIdentity(ident);
                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -3208,11 +3169,14 @@
         ImeTracing.getInstance().triggerManagerServiceDump(
                 "InputMethodManagerService#hideSoftInput", mDumper);
         synchronized (ImfLock.class) {
+            // TODO(b/305849394): Infer userId from windowToken
+            final int userId = mCurrentUserId;
             final long ident = Binder.clearCallingIdentity();
             try {
                 if (DEBUG) Slog.v(TAG, "Client requesting input be hidden");
                 return hideCurrentInputLocked(windowToken, null /* statsToken */, 0 /* flags */,
-                        null /* resultReceiver */, SoftInputShowHideReason.HIDE_SOFT_INPUT);
+                        null /* resultReceiver */, SoftInputShowHideReason.HIDE_SOFT_INPUT,
+                        userId);
             } finally {
                 Binder.restoreCallingIdentity(ident);
                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -3342,7 +3306,7 @@
                         return false;
                     }
                     if (DEBUG) Slog.v(TAG, "Client requesting Stylus Handwriting to be started");
-                    final IInputMethodInvoker curMethod = getCurMethodLocked();
+                    final IInputMethodInvoker curMethod = bindingController.getCurMethod();
                     if (curMethod != null) {
                         curMethod.canStartStylusHandwriting(requestId.getAsInt(),
                                 connectionlessCallback, cursorAnchorInfo,
@@ -3414,8 +3378,9 @@
             return false;
         }
         synchronized (ImfLock.class) {
+            final var bindingController = getInputMethodBindingController(userId);
             if (mHwController.isDelegationUsingConnectionlessFlow()) {
-                final IInputMethodInvoker curMethod = getCurMethodLocked();
+                final IInputMethodInvoker curMethod = bindingController.getCurMethod();
                 if (curMethod == null) {
                     return false;
                 }
@@ -3465,7 +3430,9 @@
             Objects.requireNonNull(windowToken, "windowToken must not be null");
             synchronized (ImfLock.class) {
                 Boolean windowPerceptible = mFocusedWindowPerceptible.get(windowToken);
-                if (mImeBindingState.mFocusedWindow != windowToken
+                final int userId = mCurrentUserId;
+                final var userData = getUserData(userId);
+                if (userData.mImeBindingState.mFocusedWindow != windowToken
                         || (windowPerceptible != null && windowPerceptible == perceptible)) {
                     return;
                 }
@@ -3477,17 +3444,18 @@
 
     @GuardedBy("ImfLock.class")
     private boolean showCurrentInputLocked(IBinder windowToken,
-            @InputMethodManager.ShowFlags int flags, @SoftInputShowHideReason int reason) {
-        final var statsToken = createStatsTokenForFocusedClient(true /* show */, reason);
+            @InputMethodManager.ShowFlags int flags, @SoftInputShowHideReason int reason,
+            @UserIdInt int userId) {
+        final var statsToken = createStatsTokenForFocusedClient(true /* show */, reason, userId);
         return showCurrentInputLocked(windowToken, statsToken, flags,
-                MotionEvent.TOOL_TYPE_UNKNOWN, null /* resultReceiver */, reason);
+                MotionEvent.TOOL_TYPE_UNKNOWN, null /* resultReceiver */, reason, userId);
     }
 
     @GuardedBy("ImfLock.class")
     boolean showCurrentInputLocked(IBinder windowToken,
             @NonNull ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags,
             @MotionEvent.ToolType int lastClickToolType, @Nullable ResultReceiver resultReceiver,
-            @SoftInputShowHideReason int reason) {
+            @SoftInputShowHideReason int reason, @UserIdInt int userId) {
         if (!mVisibilityStateComputer.onImeShowFlags(statsToken, flags)) {
             return false;
         }
@@ -3500,22 +3468,24 @@
 
         mVisibilityStateComputer.requestImeVisibility(windowToken, true);
 
-        final int userId = mCurrentUserId;
         // Ensure binding the connection when IME is going to show.
         final var bindingController = getInputMethodBindingController(userId);
+        final var userData = getUserData(userId);
         bindingController.setCurrentMethodVisible();
         final IInputMethodInvoker curMethod = bindingController.getCurMethod();
-        ImeTracker.forLogging().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+        ImeTracker.forLogging().onCancelled(userData.mCurStatsToken,
+                ImeTracker.PHASE_SERVER_WAIT_IME);
         final boolean readyToDispatchToIme;
         if (Flags.deferShowSoftInputUntilSessionCreation()) {
             readyToDispatchToIme =
-                    curMethod != null && mCurClient != null && mCurClient.mCurSession != null;
+                    curMethod != null && userData.mCurClient != null
+                            && userData.mCurClient.mCurSession != null;
         } else {
             readyToDispatchToIme = curMethod != null;
         }
         if (readyToDispatchToIme) {
             ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_HAS_IME);
-            mCurStatsToken = null;
+            userData.mCurStatsToken = null;
 
             if (Flags.useHandwritingListenerForTooltype()) {
                 maybeReportToolType();
@@ -3529,7 +3499,7 @@
             return true;
         } else {
             ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
-            mCurStatsToken = statsToken;
+            userData.mCurStatsToken = statsToken;
         }
         return false;
     }
@@ -3575,16 +3545,22 @@
                 }
                 return false;
             }
+            // TODO(b/305849394): Create a utility method for the following policy.
+            final int userId = mExperimentalConcurrentMultiUserModeEnabled
+                    ? UserHandle.getCallingUserId() : mCurrentUserId;
             final long ident = Binder.clearCallingIdentity();
+            final var userData = getUserData(userId);
             try {
                 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideSoftInput");
                 if (DEBUG) Slog.v(TAG, "Client requesting input be hidden");
                 if (Flags.refactorInsetsController()) {
-                    if (mImeBindingState != null && mImeBindingState.mFocusedWindowClient != null
-                            && mImeBindingState.mFocusedWindowClient.mClient != null) {
+                    if (userData.mImeBindingState != null
+                            && userData.mImeBindingState.mFocusedWindowClient != null
+                            && userData.mImeBindingState.mFocusedWindowClient.mClient != null) {
                         boolean wasVisible = isInputShownLocked();
                         // TODO add windowToken to interface
-                        mImeBindingState.mFocusedWindowClient.mClient.setImeVisibility(false);
+                        userData.mImeBindingState.mFocusedWindowClient.mClient
+                                .setImeVisibility(false);
                         if (resultReceiver != null) {
                             resultReceiver.send(wasVisible ? InputMethodManager.RESULT_HIDDEN
                                     : InputMethodManager.RESULT_UNCHANGED_HIDDEN, null);
@@ -3594,7 +3570,7 @@
                     return false;
                 } else {
                     return InputMethodManagerService.this.hideCurrentInputLocked(windowToken,
-                            statsToken, flags, resultReceiver, reason);
+                            statsToken, flags, resultReceiver, reason, userId);
                 }
             } finally {
                 Binder.restoreCallingIdentity(ident);
@@ -3607,23 +3583,27 @@
     @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.TEST_INPUT_METHOD)
     public void hideSoftInputFromServerForTest() {
         synchronized (ImfLock.class) {
-            hideCurrentInputLocked(mImeBindingState.mFocusedWindow, 0 /* flags */,
-                    SoftInputShowHideReason.HIDE_SOFT_INPUT);
+            // TODO(b/305849394): Get userId from caller.
+            final int userId = mCurrentUserId;
+            final var userData = getUserData(userId);
+            hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */,
+                    SoftInputShowHideReason.HIDE_SOFT_INPUT, userId);
         }
     }
 
     @GuardedBy("ImfLock.class")
     private boolean hideCurrentInputLocked(IBinder windowToken,
-            @InputMethodManager.HideFlags int flags, @SoftInputShowHideReason int reason) {
-        final var statsToken = createStatsTokenForFocusedClient(false /* show */, reason);
+            @InputMethodManager.HideFlags int flags, @SoftInputShowHideReason int reason,
+            @UserIdInt int userId) {
+        final var statsToken = createStatsTokenForFocusedClient(false /* show */, reason, userId);
         return hideCurrentInputLocked(windowToken, statsToken, flags, null /* resultReceiver */,
-                reason);
+                reason, userId);
     }
 
     @GuardedBy("ImfLock.class")
     boolean hideCurrentInputLocked(IBinder windowToken, @NonNull ImeTracker.Token statsToken,
             @InputMethodManager.HideFlags int flags, @Nullable ResultReceiver resultReceiver,
-            @SoftInputShowHideReason int reason) {
+            @SoftInputShowHideReason int reason, @UserIdInt int userId) {
         if (!mVisibilityStateComputer.canHideIme(statsToken, flags)) {
             return false;
         }
@@ -3636,8 +3616,8 @@
         // since Android Eclair.  That's why we need to accept IMM#hideSoftInput() even when only
         // IMMS#InputShown indicates that the software keyboard is shown.
         // TODO(b/246309664): Clean up IMMS#mImeWindowVis
-        final int userId = mCurrentUserId;
         final var bindingController = getInputMethodBindingController(userId);
+        final var userData = getUserData(userId);
         IInputMethodInvoker curMethod = bindingController.getCurMethod();
         final boolean shouldHideSoftInput = curMethod != null
                 && (isInputShownLocked() || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0);
@@ -3657,8 +3637,9 @@
         bindingController.setCurrentMethodNotVisible();
         mVisibilityStateComputer.clearImeShowFlags();
         // Cancel existing statsToken for show IME as we got a hide request.
-        ImeTracker.forLogging().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
-        mCurStatsToken = null;
+        ImeTracker.forLogging().onCancelled(userData.mCurStatsToken,
+                ImeTracker.PHASE_SERVER_WAIT_IME);
+        userData.mCurStatsToken = null;
         return shouldHideSoftInput;
     }
 
@@ -3728,7 +3709,7 @@
                     return new InputBindResult(
                             InputBindResult.ResultCode.ERROR_SYSTEM_NOT_READY,
                             null /* method */, null /* accessibilitySessions */, null /* channel */,
-                            getSelectedMethodIdLocked(),
+                            bindingController.getSelectedMethodId(),
                             bindingController.getSequenceNumber(),
                             false /* isInputMethodSuppressingSpellChecker */);
                 }
@@ -3788,7 +3769,8 @@
                     final boolean shouldClearFlag =
                             mImePlatformCompatUtils.shouldClearShowForcedFlag(cs.mUid);
                     final boolean showForced = mVisibilityStateComputer.mShowForced;
-                    if (mImeBindingState.mFocusedWindow != windowToken
+                    final var userData = getUserData(userId);
+                    if (userData.mImeBindingState.mFocusedWindow != windowToken
                             && showForced && shouldClearFlag) {
                         mVisibilityStateComputer.mShowForced = false;
                     }
@@ -3807,8 +3789,8 @@
                         Slog.w(TAG, "If you need to impersonate a foreground user/profile from"
                                 + " a background user, use EditorInfo.targetInputMethodUser with"
                                 + " INTERACT_ACROSS_USERS_FULL permission.");
-                        hideCurrentInputLocked(mImeBindingState.mFocusedWindow, 0 /* flags */,
-                                SoftInputShowHideReason.HIDE_INVALID_USER);
+                        hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
+                                0 /* flags */, SoftInputShowHideReason.HIDE_INVALID_USER, userId);
                         return InputBindResult.INVALID_USER;
                     }
 
@@ -3868,7 +3850,9 @@
                     + " cs=" + cs);
         }
 
-        final boolean sameWindowFocused = mImeBindingState.mFocusedWindow == windowToken;
+        final int userId = bindingController.getUserId();
+        final var userData = getUserData(userId);
+        final boolean sameWindowFocused = userData.mImeBindingState.mFocusedWindow == windowToken;
         final boolean isTextEditor = (startInputFlags & StartInputFlags.IS_TEXT_EDITOR) != 0;
         final boolean startInputByWinGainedFocus =
                 (startInputFlags & StartInputFlags.WINDOW_GAINED_FOCUS) != 0;
@@ -3900,7 +3884,7 @@
                     null, null, null, null, -1, false);
         }
 
-        mImeBindingState = new ImeBindingState(bindingController.mUserId, windowToken,
+        userData.mImeBindingState = new ImeBindingState(bindingController.getUserId(), windowToken,
                 softInputMode, cs, editorInfo);
         mFocusedWindowPerceptible.put(windowToken, true);
 
@@ -3931,16 +3915,17 @@
                     }
                     break;
             }
-            final var statsToken = createStatsTokenForFocusedClient(isShow, imeVisRes.getReason());
-            mVisibilityApplier.applyImeVisibility(mImeBindingState.mFocusedWindow, statsToken,
-                    imeVisRes.getState(), imeVisRes.getReason());
+            final var statsToken = createStatsTokenForFocusedClient(isShow, imeVisRes.getReason(),
+                    userId);
+            mVisibilityApplier.applyImeVisibility(userData.mImeBindingState.mFocusedWindow,
+                    statsToken, imeVisRes.getState(), imeVisRes.getReason(), userId);
             if (imeVisRes.getReason() == SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW) {
                 // If focused display changed, we should unbind current method
                 // to make app window in previous display relayout after Ime
                 // window token removed.
                 // Note that we can trust client's display ID as long as it matches
                 // to the display ID obtained from the window.
-                if (cs.mSelfReportedDisplayId != getCurTokenDisplayIdLocked()) {
+                if (cs.mSelfReportedDisplayId != bindingController.getCurTokenDisplayId()) {
                     bindingController.unbindCurrentMethod();
                 }
             }
@@ -3961,8 +3946,11 @@
     @GuardedBy("ImfLock.class")
     private boolean canInteractWithImeLocked(int uid, IInputMethodClient client, String methodName,
             @Nullable ImeTracker.Token statsToken) {
-        if (mCurClient == null || client == null
-                || mCurClient.mClient.asBinder() != client.asBinder()) {
+        // TODO(b/305849394): Get userId from callers.
+        final int userId = mCurrentUserId;
+        final var userData = getUserData(userId);
+        if (userData.mCurClient == null || client == null
+                || userData.mCurClient.mClient.asBinder() != client.asBinder()) {
             // We need to check if this is the current client with
             // focus in the window manager, to allow this call to
             // be made before input is started in it.
@@ -3972,7 +3960,7 @@
                 throw new IllegalArgumentException("unknown client " + client.asBinder());
             }
             ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_CLIENT_KNOWN);
-            if (!isImeClientFocused(mImeBindingState.mFocusedWindow, cs)) {
+            if (!isImeClientFocused(userData.mImeBindingState.mFocusedWindow, cs)) {
                 Slog.w(TAG, String.format("Ignoring %s of uid %d : %s", methodName, uid, client));
                 return false;
             }
@@ -3984,14 +3972,18 @@
     @GuardedBy("ImfLock.class")
     private boolean canShowInputMethodPickerLocked(IInputMethodClient client) {
         final int uid = Binder.getCallingUid();
-        if (mImeBindingState.mFocusedWindowClient != null && client != null
-                && mImeBindingState.mFocusedWindowClient.mClient.asBinder() == client.asBinder()) {
+        // TODO(b/305849394): Get userId from callers.
+        final int userId = mCurrentUserId;
+        final var userData = getUserData(userId);
+        if (userData.mImeBindingState.mFocusedWindowClient != null && client != null
+                && userData.mImeBindingState.mFocusedWindowClient.mClient.asBinder()
+                == client.asBinder()) {
             return true;
         }
-        if (mCurrentUserId != UserHandle.getUserId(uid)) {
+        if (userId != UserHandle.getUserId(uid)) {
             return false;
         }
-        final var curIntent = getInputMethodBindingController(mCurrentUserId).getCurIntent();
+        final var curIntent = getInputMethodBindingController(userId).getCurIntent();
         if (curIntent != null && InputMethodUtils.checkIfPackageBelongsToUid(
                 mPackageManagerInternal, uid, curIntent.getComponent().getPackageName())) {
             return true;
@@ -4008,11 +4000,14 @@
                         + Binder.getCallingUid() + ": " + client);
                 return;
             }
-
+            // TODO(b/305849394): Create a utility method for the following policy.
+            final int userId = mExperimentalConcurrentMultiUserModeEnabled
+                    ? UserHandle.getCallingUserId() : mCurrentUserId;
+            final var userData = getUserData(userId);
             // Always call subtype picker, because subtype picker is a superset of input method
             // picker.
-            final int displayId =
-                    (mCurClient != null) ? mCurClient.mSelfReportedDisplayId : DEFAULT_DISPLAY;
+            final int displayId = (userData.mCurClient != null)
+                    ? userData.mCurClient.mSelfReportedDisplayId : DEFAULT_DISPLAY;
             mHandler.obtainMessage(MSG_SHOW_IM_SUBTYPE_PICKER, auxiliarySubtypeMode, displayId)
                     .sendToTarget();
         }
@@ -4044,33 +4039,31 @@
     }
 
     @BinderThread
-    private void setInputMethod(@NonNull IBinder token, String id) {
+    private void setInputMethod(@NonNull IBinder token, String id, @UserIdInt int userId) {
         final int callingUid = Binder.getCallingUid();
-        final int userId = UserHandle.getUserId(callingUid);
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token)) {
+            if (!calledWithValidTokenLocked(token, userId)) {
                 return;
             }
-            final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId);
+            final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
             final InputMethodInfo imi = settings.getMethodMap().get(id);
             if (imi == null || !canCallerAccessInputMethod(
                     imi.getPackageName(), callingUid, userId, settings)) {
                 throw getExceptionForUnknownImeId(id);
             }
-            setInputMethodWithSubtypeIdLocked(token, id, NOT_A_SUBTYPE_ID);
+            setInputMethodWithSubtypeIdLocked(token, id, NOT_A_SUBTYPE_ID, userId);
         }
     }
 
     @BinderThread
     private void setInputMethodAndSubtype(@NonNull IBinder token, String id,
-            InputMethodSubtype subtype) {
+            InputMethodSubtype subtype, @UserIdInt int userId) {
         final int callingUid = Binder.getCallingUid();
-        final int userId = UserHandle.getUserId(callingUid);
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token)) {
+            if (!calledWithValidTokenLocked(token, userId)) {
                 return;
             }
-            final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId);
+            final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
             final InputMethodInfo imi = settings.getMethodMap().get(id);
             if (imi == null || !canCallerAccessInputMethod(
                     imi.getPackageName(), callingUid, userId, settings)) {
@@ -4078,20 +4071,19 @@
             }
             if (subtype != null) {
                 setInputMethodWithSubtypeIdLocked(token, id,
-                        SubtypeUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode()));
+                        SubtypeUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode()), userId);
             } else {
-                setInputMethod(token, id);
+                setInputMethod(token, id, userId);
             }
         }
     }
 
     @BinderThread
-    private boolean switchToPreviousInputMethod(@NonNull IBinder token) {
+    private boolean switchToPreviousInputMethod(@NonNull IBinder token, @UserIdInt int userId) {
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token)) {
+            if (!calledWithValidTokenLocked(token, userId)) {
                 return false;
             }
-            final int userId = mCurrentUserId;
             final var bindingController = getInputMethodBindingController(userId);
             final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
             final Pair<String, String> lastIme = settings.getLastInputMethodAndSubtype();
@@ -4156,9 +4148,10 @@
             if (!TextUtils.isEmpty(targetLastImiId)) {
                 if (DEBUG) {
                     Slog.d(TAG, "Switch to: " + lastImi.getId() + ", " + lastIme.second
-                            + ", from: " + getSelectedMethodIdLocked() + ", " + subtypeId);
+                            + ", from: " + bindingController.getSelectedMethodId() + ", "
+                            + subtypeId);
                 }
-                setInputMethodWithSubtypeIdLocked(token, targetLastImiId, subtypeId);
+                setInputMethodWithSubtypeIdLocked(token, targetLastImiId, subtypeId, userId);
                 return true;
             } else {
                 return false;
@@ -4167,18 +4160,19 @@
     }
 
     @BinderThread
-    private boolean switchToNextInputMethod(@NonNull IBinder token, boolean onlyCurrentIme) {
+    private boolean switchToNextInputMethod(@NonNull IBinder token, boolean onlyCurrentIme,
+            @UserIdInt int userId) {
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token)) {
+            if (!calledWithValidTokenLocked(token, userId)) {
                 return false;
             }
-            return switchToNextInputMethodLocked(token, onlyCurrentIme);
+            return switchToNextInputMethodLocked(token, onlyCurrentIme, userId);
         }
     }
 
     @GuardedBy("ImfLock.class")
-    private boolean switchToNextInputMethodLocked(@Nullable IBinder token, boolean onlyCurrentIme) {
-        final int userId = mCurrentUserId;
+    private boolean switchToNextInputMethodLocked(@Nullable IBinder token, boolean onlyCurrentIme,
+            @UserIdInt int userId) {
         final var bindingController = getInputMethodBindingController(userId);
         final var currentImi = bindingController.getSelectedMethod();
         final ImeSubtypeListItem nextSubtype = getUserData(userId).mSwitchingController
@@ -4188,17 +4182,17 @@
             return false;
         }
         setInputMethodWithSubtypeIdLocked(token, nextSubtype.mImi.getId(),
-                nextSubtype.mSubtypeId);
+                nextSubtype.mSubtypeId, userId);
         return true;
     }
 
     @BinderThread
-    private boolean shouldOfferSwitchingToNextInputMethod(@NonNull IBinder token) {
+    private boolean shouldOfferSwitchingToNextInputMethod(@NonNull IBinder token,
+            @UserIdInt int userId) {
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token)) {
+            if (!calledWithValidTokenLocked(token, userId)) {
                 return false;
             }
-            final int userId = mCurrentUserId;
             final var bindingController = getInputMethodBindingController(userId);
             final var currentImi = bindingController.getSelectedMethod();
             final ImeSubtypeListItem nextSubtype = getUserData(userId).mSwitchingController
@@ -4260,7 +4254,8 @@
                             DirectBootAwareness.AUTO);
                     InputMethodSettingsRepository.put(userId, newSettings);
                     if (isCurrentUser) {
-                        postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */);
+                        postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */,
+                                userId);
                     }
                 } finally {
                     Binder.restoreCallingIdentity(ident);
@@ -4299,7 +4294,7 @@
                     if (mSettingsObserver != null) {
                         mSettingsObserver.mLastEnabled = settings.getEnabledInputMethodsStr();
                     }
-                    updateInputMethodsFromSettingsLocked(false /* enabledChanged */);
+                    updateInputMethodsFromSettingsLocked(false /* enabledChanged */, userId);
                 }
             }
         } finally {
@@ -4318,7 +4313,8 @@
     @Override
     @Deprecated
     public int getInputMethodWindowVisibleHeight(@NonNull IInputMethodClient client) {
-        int callingUid = Binder.getCallingUid();
+        final int callingUid = Binder.getCallingUid();
+        final int callingUserId = UserHandle.getCallingUserId();
         return Binder.withCleanCallingIdentity(() -> {
             final int curTokenDisplayId;
             synchronized (ImfLock.class) {
@@ -4326,9 +4322,13 @@
                         "getInputMethodWindowVisibleHeight", null /* statsToken */)) {
                     return 0;
                 }
+                // TODO(b/305849394): Create a utility method for the following policy.
+                final int userId = mExperimentalConcurrentMultiUserModeEnabled
+                        ? callingUserId : mCurrentUserId;
+                final var bindingController = getInputMethodBindingController(userId);
                 // This should probably use the caller's display id, but because this is unsupported
                 // and maintained only for compatibility, there's no point in fixing it.
-                curTokenDisplayId = getCurTokenDisplayIdLocked();
+                curTokenDisplayId = bindingController.getCurTokenDisplayId();
             }
             return mWindowManagerInternal.getInputMethodWindowVisibleHeight(curTokenDisplayId);
         });
@@ -4593,27 +4593,29 @@
 
     private void dumpDebug(ProtoOutputStream proto, long fieldId) {
         synchronized (ImfLock.class) {
-            final var bindingController = getInputMethodBindingController(mCurrentUserId);
+            final int userId = mCurrentUserId;
+            final var bindingController = getInputMethodBindingController(userId);
+            final var userData = getUserData(userId);
             final long token = proto.start(fieldId);
-            proto.write(CUR_METHOD_ID, getSelectedMethodIdLocked());
+            proto.write(CUR_METHOD_ID, bindingController.getSelectedMethodId());
             proto.write(CUR_SEQ, bindingController.getSequenceNumber());
-            proto.write(CUR_CLIENT, Objects.toString(mCurClient));
-            mImeBindingState.dumpDebug(proto, mWindowManagerInternal);
+            proto.write(CUR_CLIENT, Objects.toString(userData.mCurClient));
+            userData.mImeBindingState.dumpDebug(proto, mWindowManagerInternal);
             proto.write(LAST_IME_TARGET_WINDOW_NAME,
                     mWindowManagerInternal.getWindowName(mLastImeTargetWindow));
             proto.write(CUR_FOCUSED_WINDOW_SOFT_INPUT_MODE, InputMethodDebug.softInputModeToString(
-                    mImeBindingState.mFocusedWindowSoftInputMode));
-            if (mCurEditorInfo != null) {
-                mCurEditorInfo.dumpDebug(proto, CUR_ATTRIBUTE);
+                    userData.mImeBindingState.mFocusedWindowSoftInputMode));
+            if (userData.mCurEditorInfo != null) {
+                userData.mCurEditorInfo.dumpDebug(proto, CUR_ATTRIBUTE);
             }
             proto.write(CUR_ID, bindingController.getCurId());
             mVisibilityStateComputer.dumpDebug(proto, fieldId);
-            proto.write(IN_FULLSCREEN_MODE, mInFullscreenMode);
-            proto.write(CUR_TOKEN, Objects.toString(getCurTokenLocked()));
-            proto.write(CUR_TOKEN_DISPLAY_ID, getCurTokenDisplayIdLocked());
+            proto.write(IN_FULLSCREEN_MODE, userData.mInFullscreenMode);
+            proto.write(CUR_TOKEN, Objects.toString(bindingController.getCurToken()));
+            proto.write(CUR_TOKEN_DISPLAY_ID, bindingController.getCurTokenDisplayId());
             proto.write(SYSTEM_READY, mSystemReady);
             proto.write(HAVE_CONNECTION, bindingController.hasMainConnection());
-            proto.write(BOUND_TO_METHOD, mBoundToMethod);
+            proto.write(BOUND_TO_METHOD, userData.mBoundToMethod);
             proto.write(IS_INTERACTIVE, mIsInteractive);
             proto.write(BACK_DISPOSITION, mBackDisposition);
             proto.write(IME_WINDOW_VISIBILITY, mImeWindowVis);
@@ -4623,20 +4625,19 @@
     }
 
     @BinderThread
-    private void notifyUserAction(@NonNull IBinder token) {
+    private void notifyUserAction(@NonNull IBinder token, @UserIdInt int userId) {
         if (DEBUG) {
             Slog.d(TAG, "Got the notification of a user action.");
         }
         synchronized (ImfLock.class) {
-            if (getCurTokenLocked() != token) {
+            final var bindingController = getInputMethodBindingController(userId);
+            if (bindingController.getCurToken() != token) {
                 if (DEBUG) {
                     Slog.d(TAG, "Ignoring the user action notification from IMEs that are no longer"
                             + " active.");
                 }
                 return;
             }
-            final int userId = mCurrentUserId;
-            final var bindingController = getInputMethodBindingController(userId);
             final InputMethodInfo imi = bindingController.getSelectedMethod();
             if (imi != null) {
                 getUserData(userId).mSwitchingController.onUserActionLocked(imi,
@@ -4647,11 +4648,11 @@
 
     @BinderThread
     private void applyImeVisibility(IBinder token, IBinder windowToken, boolean setVisible,
-            @NonNull ImeTracker.Token statsToken) {
+            @NonNull ImeTracker.Token statsToken, @UserIdInt int userId) {
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.applyImeVisibility");
             synchronized (ImfLock.class) {
-                if (!calledWithValidTokenLocked(token)) {
+                if (!calledWithValidTokenLocked(token, userId)) {
                     ImeTracker.forLogging().onFailed(statsToken,
                             ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME);
                     return;
@@ -4659,10 +4660,11 @@
                 ImeTracker.forLogging().onProgress(statsToken,
                         ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME);
                 final IBinder requestToken = mVisibilityStateComputer.getWindowTokenFrom(
-                        windowToken);
+                        windowToken, userId);
                 mVisibilityApplier.applyImeVisibility(requestToken, statsToken,
                         setVisible ? ImeVisibilityStateComputer.STATE_SHOW_IME
-                                : ImeVisibilityStateComputer.STATE_HIDE_IME, mCurrentUserId);
+                                : ImeVisibilityStateComputer.STATE_HIDE_IME,
+                        SoftInputShowHideReason.NOT_SET /* ignore reason */, userId);
             }
         } finally {
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -4683,7 +4685,9 @@
     }
 
     @GuardedBy("ImfLock.class")
-    private void setInputMethodWithSubtypeIdLocked(IBinder token, String id, int subtypeId) {
+    private void setInputMethodWithSubtypeIdLocked(IBinder token, String id, int subtypeId,
+            @UserIdInt int userId) {
+        final var bindingController = getInputMethodBindingController(userId);
         if (token == null) {
             if (mContext.checkCallingOrSelfPermission(
                     android.Manifest.permission.WRITE_SECURE_SETTINGS)
@@ -4692,7 +4696,7 @@
                         "Using null token requires permission "
                                 + android.Manifest.permission.WRITE_SECURE_SETTINGS);
             }
-        } else if (getCurTokenLocked() != token) {
+        } else if (bindingController.getCurToken() != token) {
             Slog.w(TAG, "Ignoring setInputMethod of uid " + Binder.getCallingUid()
                     + " token: " + token);
             return;
@@ -4708,7 +4712,7 @@
 
         final long ident = Binder.clearCallingIdentity();
         try {
-            setInputMethodLocked(id, subtypeId);
+            setInputMethodLocked(id, subtypeId, userId);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -4721,17 +4725,20 @@
     void onShowHideSoftInputRequested(boolean show, IBinder requestImeToken,
             @SoftInputShowHideReason int reason, @Nullable ImeTracker.Token statsToken,
             @UserIdInt int userId) {
-        final IBinder requestToken = mVisibilityStateComputer.getWindowTokenFrom(requestImeToken);
+        final IBinder requestToken = mVisibilityStateComputer.getWindowTokenFrom(requestImeToken,
+                userId);
         final var bindingController = getInputMethodBindingController(userId);
+        final var userData = getUserData(userId);
         final WindowManagerInternal.ImeTargetInfo info =
                 mWindowManagerInternal.onToggleImeRequested(
-                        show, mImeBindingState.mFocusedWindow, requestToken,
+                        show, userData.mImeBindingState.mFocusedWindow, requestToken,
                         bindingController.getCurTokenDisplayId());
         mSoftInputShowHideHistory.addEntry(new SoftInputShowHideHistory.Entry(
-                mImeBindingState.mFocusedWindowClient, mImeBindingState.mFocusedWindowEditorInfo,
-                info.focusedWindowName, mImeBindingState.mFocusedWindowSoftInputMode, reason,
-                mInFullscreenMode, info.requestWindowName, info.imeControlTargetName,
-                info.imeLayerTargetName, info.imeSurfaceParentName));
+                userData.mImeBindingState.mFocusedWindowClient,
+                userData.mImeBindingState.mFocusedWindowEditorInfo,
+                info.focusedWindowName, userData.mImeBindingState.mFocusedWindowSoftInputMode,
+                reason, userData.mInFullscreenMode, info.requestWindowName,
+                info.imeControlTargetName, info.imeLayerTargetName, info.imeSurfaceParentName));
 
         if (statsToken != null) {
             mImeTrackerService.onImmsUpdate(statsToken, info.requestWindowName);
@@ -4740,30 +4747,33 @@
 
     @BinderThread
     private void hideMySoftInput(@NonNull IBinder token, @NonNull ImeTracker.Token statsToken,
-            @InputMethodManager.HideFlags int flags, @SoftInputShowHideReason int reason) {
+            @InputMethodManager.HideFlags int flags, @SoftInputShowHideReason int reason,
+            @UserIdInt int userId) {
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideMySoftInput");
             synchronized (ImfLock.class) {
-                if (!calledWithValidTokenLocked(token)) {
+                if (!calledWithValidTokenLocked(token, userId)) {
                     ImeTracker.forLogging().onFailed(statsToken,
                             ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME);
                     return;
                 }
+                final var userData = getUserData(userId);
                 ImeTracker.forLogging().onProgress(statsToken,
                         ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME);
                 final long ident = Binder.clearCallingIdentity();
                 try {
                     if (Flags.refactorInsetsController()) {
-                        mCurClient.mClient.setImeVisibility(false);
+                        userData.mCurClient.mClient.setImeVisibility(false);
                         // TODO we will loose the flags here
-                        if (mImeBindingState != null
-                                && mImeBindingState.mFocusedWindowClient != null
-                                && mImeBindingState.mFocusedWindowClient.mClient != null) {
-                            mImeBindingState.mFocusedWindowClient.mClient.setImeVisibility(false);
+                        if (userData.mImeBindingState != null
+                                && userData.mImeBindingState.mFocusedWindowClient != null
+                                && userData.mImeBindingState.mFocusedWindowClient.mClient != null) {
+                            userData.mImeBindingState.mFocusedWindowClient.mClient
+                                    .setImeVisibility(false);
                         }
                     } else {
                         hideCurrentInputLocked(mLastImeTargetWindow, statsToken, flags,
-                                null /* resultReceiver */, reason);
+                                null /* resultReceiver */, reason, userId);
                     }
                 } finally {
                     Binder.restoreCallingIdentity(ident);
@@ -4776,30 +4786,34 @@
 
     @BinderThread
     private void showMySoftInput(@NonNull IBinder token, @NonNull ImeTracker.Token statsToken,
-            @InputMethodManager.ShowFlags int flags, @SoftInputShowHideReason int reason) {
+            @InputMethodManager.ShowFlags int flags, @SoftInputShowHideReason int reason,
+            @UserIdInt int userId) {
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showMySoftInput");
             synchronized (ImfLock.class) {
-                if (!calledWithValidTokenLocked(token)) {
+                if (!calledWithValidTokenLocked(token, userId)) {
                     ImeTracker.forLogging().onFailed(statsToken,
                             ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME);
                     return;
                 }
+                final var userData = getUserData(userId);
                 ImeTracker.forLogging().onProgress(statsToken,
                         ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME);
                 final long ident = Binder.clearCallingIdentity();
                 try {
                     if (Flags.refactorInsetsController()) {
-                        mCurClient.mClient.setImeVisibility(false);
+                        userData.mCurClient.mClient.setImeVisibility(false);
                         // TODO we will loose the flags here
-                        if (mImeBindingState != null
-                                && mImeBindingState.mFocusedWindowClient != null
-                                && mImeBindingState.mFocusedWindowClient.mClient != null) {
-                            mImeBindingState.mFocusedWindowClient.mClient.setImeVisibility(true);
+                        if (userData.mImeBindingState != null
+                                && userData.mImeBindingState.mFocusedWindowClient != null
+                                && userData.mImeBindingState.mFocusedWindowClient.mClient != null) {
+                            userData.mImeBindingState.mFocusedWindowClient.mClient
+                                    .setImeVisibility(true);
                         }
                     } else {
                         showCurrentInputLocked(mLastImeTargetWindow, statsToken, flags,
-                                MotionEvent.TOOL_TYPE_UNKNOWN, null /* resultReceiver */, reason);
+                                MotionEvent.TOOL_TYPE_UNKNOWN, null /* resultReceiver */, reason,
+                                userId);
                     }
                 } finally {
                     Binder.restoreCallingIdentity(ident);
@@ -4810,46 +4824,52 @@
         }
     }
 
+    @GuardedBy("ImfLock.class")
     @VisibleForTesting
-    ImeVisibilityApplier getVisibilityApplier() {
-        synchronized (ImfLock.class) {
-            return mVisibilityApplier;
-        }
+    DefaultImeVisibilityApplier getVisibilityApplierLocked() {
+        return mVisibilityApplier;
     }
 
     void onApplyImeVisibilityFromComputer(IBinder windowToken, @NonNull ImeTracker.Token statsToken,
             @NonNull ImeVisibilityResult result) {
         synchronized (ImfLock.class) {
+            // TODO(b/305849394): Infer userId from windowToken
+            final int userId = mCurrentUserId;
             mVisibilityApplier.applyImeVisibility(windowToken, statsToken, result.getState(),
-                    result.getReason());
+                    result.getReason(), userId);
         }
     }
 
     @GuardedBy("ImfLock.class")
-    void setEnabledSessionLocked(SessionState session) {
-        if (mEnabledSession != session) {
-            if (mEnabledSession != null && mEnabledSession.mSession != null) {
-                if (DEBUG) Slog.v(TAG, "Disabling: " + mEnabledSession);
-                mEnabledSession.mMethod.setSessionEnabled(mEnabledSession.mSession, false);
+    void setEnabledSessionLocked(SessionState session,
+            @NonNull UserDataRepository.UserData userData) {
+        if (userData.mEnabledSession != session) {
+            if (userData.mEnabledSession != null && userData.mEnabledSession.mSession != null) {
+                if (DEBUG) Slog.v(TAG, "Disabling: " + userData.mEnabledSession);
+                userData.mEnabledSession.mMethod.setSessionEnabled(
+                        userData.mEnabledSession.mSession, false);
             }
-            mEnabledSession = session;
-            if (mEnabledSession != null && mEnabledSession.mSession != null) {
-                if (DEBUG) Slog.v(TAG, "Enabling: " + mEnabledSession);
-                mEnabledSession.mMethod.setSessionEnabled(mEnabledSession.mSession, true);
+            userData.mEnabledSession = session;
+            if (userData.mEnabledSession != null && userData.mEnabledSession.mSession != null) {
+                if (DEBUG) Slog.v(TAG, "Enabling: " + userData.mEnabledSession);
+                userData.mEnabledSession.mMethod.setSessionEnabled(
+                        userData.mEnabledSession.mSession, true);
             }
         }
     }
 
     @GuardedBy("ImfLock.class")
     void setEnabledSessionForAccessibilityLocked(
-            SparseArray<AccessibilitySessionState> accessibilitySessions) {
+            SparseArray<AccessibilitySessionState> accessibilitySessions,
+            @NonNull UserDataRepository.UserData userData) {
         // mEnabledAccessibilitySessions could the same object as accessibilitySessions.
         SparseArray<IAccessibilityInputMethodSession> disabledSessions = new SparseArray<>();
-        for (int i = 0; i < mEnabledAccessibilitySessions.size(); i++) {
-            if (!accessibilitySessions.contains(mEnabledAccessibilitySessions.keyAt(i))) {
-                AccessibilitySessionState sessionState = mEnabledAccessibilitySessions.valueAt(i);
+        for (int i = 0; i < userData.mEnabledAccessibilitySessions.size(); i++) {
+            if (!accessibilitySessions.contains(userData.mEnabledAccessibilitySessions.keyAt(i))) {
+                AccessibilitySessionState sessionState =
+                        userData.mEnabledAccessibilitySessions.valueAt(i);
                 if (sessionState != null) {
-                    disabledSessions.append(mEnabledAccessibilitySessions.keyAt(i),
+                    disabledSessions.append(userData.mEnabledAccessibilitySessions.keyAt(i),
                             sessionState.mSession);
                 }
             }
@@ -4860,7 +4880,7 @@
         }
         SparseArray<IAccessibilityInputMethodSession> enabledSessions = new SparseArray<>();
         for (int i = 0; i < accessibilitySessions.size(); i++) {
-            if (!mEnabledAccessibilitySessions.contains(accessibilitySessions.keyAt(i))) {
+            if (!userData.mEnabledAccessibilitySessions.contains(accessibilitySessions.keyAt(i))) {
                 AccessibilitySessionState sessionState = accessibilitySessions.valueAt(i);
                 if (sessionState != null) {
                     enabledSessions.append(accessibilitySessions.keyAt(i), sessionState.mSession);
@@ -4871,7 +4891,7 @@
             AccessibilityManagerInternal.get().setImeSessionEnabled(enabledSessions,
                     true);
         }
-        mEnabledAccessibilitySessions = accessibilitySessions;
+        userData.mEnabledAccessibilitySessions = accessibilitySessions;
     }
 
     @SuppressWarnings("unchecked")
@@ -4931,25 +4951,33 @@
 
             case MSG_HIDE_ALL_INPUT_METHODS:
                 synchronized (ImfLock.class) {
+                    // TODO(b/305849394): Needs to figure out what to do where for background users.
+                    final int userId = mCurrentUserId;
+                    final var userData = getUserData(userId);
                     if (Flags.refactorInsetsController()) {
-                        if (mImeBindingState != null
-                                && mImeBindingState.mFocusedWindowClient != null
-                                && mImeBindingState.mFocusedWindowClient.mClient != null) {
-                            mImeBindingState.mFocusedWindowClient.mClient.setImeVisibility(false);
+                        if (userData.mImeBindingState != null
+                                && userData.mImeBindingState.mFocusedWindowClient != null
+                                && userData.mImeBindingState.mFocusedWindowClient.mClient != null) {
+                            userData.mImeBindingState.mFocusedWindowClient.mClient
+                                    .setImeVisibility(false);
                         }
                     } else {
                         @SoftInputShowHideReason final int reason = (int) msg.obj;
-                        hideCurrentInputLocked(mImeBindingState.mFocusedWindow, 0 /* flags */,
-                                reason);
+                        hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
+                                0 /* flags */, reason, userId);
                     }
                 }
                 return true;
             case MSG_REMOVE_IME_SURFACE: {
                 synchronized (ImfLock.class) {
+                    // TODO(b/305849394): Needs to figure out what to do where for background users.
+                    final int userId = mCurrentUserId;
+                    final var userData = getUserData(userId);
                     try {
-                        if (mEnabledSession != null && mEnabledSession.mSession != null
-                                && !isShowRequestedForCurrentWindow()) {
-                            mEnabledSession.mSession.removeImeSurface();
+                        if (userData.mEnabledSession != null
+                                && userData.mEnabledSession.mSession != null
+                                && !isShowRequestedForCurrentWindow(userId)) {
+                            userData.mEnabledSession.mSession.removeImeSurface();
                         }
                     } catch (RemoteException e) {
                     }
@@ -4959,10 +4987,14 @@
             case MSG_REMOVE_IME_SURFACE_FROM_WINDOW: {
                 IBinder windowToken = (IBinder) msg.obj;
                 synchronized (ImfLock.class) {
+                    // TODO(b/305849394): Infer userId from windowToken.
+                    final int userId = mCurrentUserId;
+                    final var userData = getUserData(userId);
                     try {
-                        if (windowToken == mImeBindingState.mFocusedWindow
-                                && mEnabledSession != null && mEnabledSession.mSession != null) {
-                            mEnabledSession.mSession.removeImeSurface();
+                        if (windowToken == userData.mImeBindingState.mFocusedWindow
+                                && userData.mEnabledSession != null
+                                && userData.mEnabledSession.mSession != null) {
+                            userData.mEnabledSession.mSession.removeImeSurface();
                         }
                     } catch (RemoteException e) {
                     }
@@ -5015,9 +5047,11 @@
                 synchronized (ImfLock.class) {
                     final var bindingController = getInputMethodBindingController(mCurrentUserId);
                     if (bindingController.supportsStylusHandwriting()
-                            && getCurMethodLocked() != null && hasSupportedStylusLocked()) {
+                            && bindingController.getCurMethod() != null
+                            && hasSupportedStylusLocked()) {
                         Slog.d(TAG, "Initializing Handwriting Spy");
-                        mHwController.initializeHandwritingSpy(getCurTokenDisplayIdLocked());
+                        mHwController.initializeHandwritingSpy(
+                                bindingController.getCurTokenDisplayId());
                     } else {
                         mHwController.reset();
                     }
@@ -5034,18 +5068,21 @@
                 }
                 return true;
             case MSG_START_HANDWRITING:
+                final var handwritingRequest = (HandwritingRequest) msg.obj;
                 synchronized (ImfLock.class) {
-                    IInputMethodInvoker curMethod = getCurMethodLocked();
-                    if (curMethod == null || mImeBindingState.mFocusedWindow == null) {
+                    final int userId = handwritingRequest.userId;
+                    final var bindingController = getInputMethodBindingController(userId);
+                    final var userData = getUserData(userId);
+                    IInputMethodInvoker curMethod = bindingController.getCurMethod();
+                    if (curMethod == null || userData.mImeBindingState.mFocusedWindow == null) {
                         return true;
                     }
-                    final var bindingController = getInputMethodBindingController(mCurrentUserId);
                     final HandwritingModeController.HandwritingSession session =
                             mHwController.startHandwritingSession(
-                                    msg.arg1 /*requestId*/,
-                                    msg.arg2 /*pid*/,
+                                    handwritingRequest.requestId,
+                                    handwritingRequest.pid,
                                     bindingController.getCurMethodUid(),
-                                    mImeBindingState.mFocusedWindow);
+                                    userData.mImeBindingState.mFocusedWindow);
                     if (session == null) {
                         Slog.e(TAG,
                                 "Failed to start handwriting session for requestId: " + msg.arg1);
@@ -5080,9 +5117,12 @@
         return false;
     }
 
+    private record HandwritingRequest(int requestId, int pid, @UserIdInt int userId) { }
+
     @BinderThread
-    private void onStylusHandwritingReady(int requestId, int pid) {
-        mHandler.obtainMessage(MSG_START_HANDWRITING, requestId, pid).sendToTarget();
+    private void onStylusHandwritingReady(int requestId, int pid, @UserIdInt int userId) {
+        mHandler.obtainMessage(MSG_START_HANDWRITING,
+                new HandwritingRequest(requestId, pid, userId)).sendToTarget();
     }
 
     private void handleSetInteractive(final boolean interactive) {
@@ -5090,8 +5130,11 @@
             mIsInteractive = interactive;
             updateSystemUiLocked(interactive ? mImeWindowVis : 0, mBackDisposition);
 
+            // TODO(b/305849394): Support multiple IMEs.
+            final var userId = mCurrentUserId;
+            final var userData = getUserData(userId);
             // Inform the current client of the change in active status
-            if (mCurClient == null || mCurClient.mClient == null) {
+            if (userData.mCurClient == null || userData.mCurClient.mClient == null) {
                 return;
             }
             // TODO(b/325515685): user data must be retrieved by a userId parameter
@@ -5101,17 +5144,19 @@
                 // Handle IME visibility when interactive changed before finishing the input to
                 // ensure we preserve the last state as possible.
                 final ImeVisibilityResult imeVisRes = mVisibilityStateComputer.onInteractiveChanged(
-                        mImeBindingState.mFocusedWindow, interactive);
+                        userData.mImeBindingState.mFocusedWindow, interactive);
                 if (imeVisRes != null) {
                     // Pass in a null statsToken as the IME snapshot is not tracked by ImeTracker.
-                    mVisibilityApplier.applyImeVisibility(mImeBindingState.mFocusedWindow,
-                            null /* statsToken */, imeVisRes.getState(), imeVisRes.getReason());
+                    mVisibilityApplier.applyImeVisibility(userData.mImeBindingState.mFocusedWindow,
+                            null /* statsToken */, imeVisRes.getState(), imeVisRes.getReason(),
+                            userId);
                 }
                 // Eligible IME processes use new "setInteractive" protocol.
-                mCurClient.mClient.setInteractive(mIsInteractive, mInFullscreenMode);
+                userData.mCurClient.mClient.setInteractive(mIsInteractive,
+                        userData.mInFullscreenMode);
             } else {
                 // Legacy IME processes continue using legacy "setActive" protocol.
-                mCurClient.mClient.setActive(mIsInteractive, mInFullscreenMode);
+                userData.mCurClient.mClient.setActive(mIsInteractive, userData.mInFullscreenMode);
             }
         }
     }
@@ -5228,7 +5273,8 @@
     }
 
     @GuardedBy("ImfLock.class")
-    void postInputMethodSettingUpdatedLocked(boolean resetDefaultEnabledIme) {
+    void postInputMethodSettingUpdatedLocked(boolean resetDefaultEnabledIme,
+            @UserIdInt int userId) {
         if (DEBUG) {
             Slog.d(TAG, "--- re-buildInputMethodList reset = " + resetDefaultEnabledIme
                     + " \n ------ caller=" + Debug.getCallers(10));
@@ -5238,7 +5284,6 @@
             return;
         }
 
-        final int userId = mCurrentUserId;
         final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
 
         boolean reenableMinimumNonAuxSystemImes = false;
@@ -5291,7 +5336,7 @@
             if (!settings.getMethodMap().containsKey(defaultImiId)) {
                 Slog.w(TAG, "Default IME is uninstalled. Choose new default IME.");
                 if (chooseNewDefaultIMELocked()) {
-                    updateInputMethodsFromSettingsLocked(true);
+                    updateInputMethodsFromSettingsLocked(true, userId);
                 }
             } else {
                 // Double check that the default IME is certainly enabled.
@@ -5417,11 +5462,10 @@
 
     @GuardedBy("ImfLock.class")
     private void setSelectedInputMethodAndSubtypeLocked(InputMethodInfo imi, int subtypeId,
-            boolean setSubtypeOnly) {
-        final int userId = mCurrentUserId;
+            boolean setSubtypeOnly, @UserIdInt int userId) {
         final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
         final var bindingController = getInputMethodBindingController(userId);
-        settings.saveCurrentInputMethodAndSubtypeToHistory(getSelectedMethodIdLocked(),
+        settings.saveCurrentInputMethodAndSubtypeToHistory(bindingController.getSelectedMethodId(),
                 bindingController.getCurrentSubtype());
 
         // Set Subtype here
@@ -5455,11 +5499,13 @@
 
     @GuardedBy("ImfLock.class")
     private void resetSelectedInputMethodAndSubtypeLocked(String newDefaultIme) {
-        final var bindingController = getInputMethodBindingController(mCurrentUserId);
+        // TODO(b/305849394): get userId from callers
+        final int userId = mCurrentUserId;
+        final var bindingController = getInputMethodBindingController(userId);
         bindingController.setDisplayIdToShowIme(INVALID_DISPLAY);
         bindingController.setDeviceIdToShowIme(DEVICE_ID_DEFAULT);
 
-        final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId);
+        final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
         settings.putSelectedDefaultDeviceInputMethod(null);
 
         InputMethodInfo imi = settings.getMethodMap().get(newDefaultIme);
@@ -5476,7 +5522,7 @@
                 }
             }
         }
-        setSelectedInputMethodAndSubtypeLocked(imi, lastSubtypeId, false);
+        setSelectedInputMethodAndSubtypeLocked(imi, lastSubtypeId, false, userId);
     }
 
     /**
@@ -5560,7 +5606,7 @@
                     .contains(settings.getMethodMap().get(imeId))) {
                 return false; // IME is not found or not enabled.
             }
-            setInputMethodLocked(imeId, NOT_A_SUBTYPE_ID);
+            setInputMethodLocked(imeId, NOT_A_SUBTYPE_ID, userId);
             return true;
         }
         if (!settings.getMethodMap().containsKey(imeId)
@@ -5602,15 +5648,15 @@
     }
 
     @GuardedBy("ImfLock.class")
-    private void switchKeyboardLayoutLocked(int direction) {
-        final int userId = mCurrentUserId;
+    private void switchKeyboardLayoutLocked(int direction, @UserIdInt int userId) {
         final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
 
-        final InputMethodInfo currentImi = settings.getMethodMap().get(getSelectedMethodIdLocked());
+        final var bindingController = getInputMethodBindingController(userId);
+        final InputMethodInfo currentImi = settings.getMethodMap().get(
+                bindingController.getSelectedMethodId());
         if (currentImi == null) {
             return;
         }
-        final var bindingController = getInputMethodBindingController(userId);
         final InputMethodSubtypeHandle currentSubtypeHandle =
                 InputMethodSubtypeHandle.of(currentImi, bindingController.getCurrentSubtype());
         final InputMethodSubtypeHandle nextSubtypeHandle =
@@ -5627,7 +5673,7 @@
         final int subtypeCount = nextImi.getSubtypeCount();
         if (subtypeCount == 0) {
             if (nextSubtypeHandle.equals(InputMethodSubtypeHandle.of(nextImi, null))) {
-                setInputMethodLocked(nextImi.getId(), NOT_A_SUBTYPE_ID);
+                setInputMethodLocked(nextImi.getId(), NOT_A_SUBTYPE_ID, userId);
             }
             return;
         }
@@ -5635,7 +5681,7 @@
         for (int i = 0; i < subtypeCount; ++i) {
             if (nextSubtypeHandle.equals(
                     InputMethodSubtypeHandle.of(nextImi, nextImi.getSubtypeAt(i)))) {
-                setInputMethodLocked(nextImi.getId(), i);
+                setInputMethodLocked(nextImi.getId(), i, userId);
                 return;
             }
         }
@@ -5752,7 +5798,8 @@
             //TODO(b/150843766): Check if Input Token is valid.
             final IBinder curHostInputToken;
             synchronized (ImfLock.class) {
-                if (displayId != getCurTokenDisplayIdLocked()) {
+                final var bindingController = getInputMethodBindingController(userId);
+                if (displayId != bindingController.getCurTokenDisplayId()) {
                     return false;
                 }
                 curHostInputToken = getInputMethodBindingController(userId).getCurHostInputToken();
@@ -5766,7 +5813,10 @@
         @Override
         public void reportImeControl(@Nullable IBinder windowToken) {
             synchronized (ImfLock.class) {
-                if (mImeBindingState.mFocusedWindow != windowToken) {
+                // TODO(b/305849394): Need to infer userId or get userId from callers.
+                final int userId = mCurrentUserId;
+                final var userData = getUserData(userId);
+                if (userData.mImeBindingState.mFocusedWindow != windowToken) {
                     // A perceptible value was set for the focused window, but it is no longer in
                     // control, so we reset the perceptible for the window passed as argument.
                     // TODO(b/314149476): Investigate whether this logic is still relevant, if not
@@ -5779,10 +5829,13 @@
         @Override
         public void onImeParentChanged(int displayId) {
             synchronized (ImfLock.class) {
+                // TODO(b/305849394): Need to infer userId or get userId from callers.
+                final int userId = mCurrentUserId;
+                final var userData = getUserData(userId);
                 // Hide the IME method menu only when the IME surface parent is changed by the
                 // input target changed, in case seeing the dialog dismiss flickering during
                 // the next focused window starting the input connection.
-                if (mLastImeTargetWindow != mImeBindingState.mFocusedWindow) {
+                if (mLastImeTargetWindow != userData.mImeBindingState.mFocusedWindow) {
                     mMenuController.hideInputMethodMenuLocked();
                 }
             }
@@ -5805,33 +5858,36 @@
         public void onSessionForAccessibilityCreated(int accessibilityConnectionId,
                 IAccessibilityInputMethodSession session, @UserIdInt int userId) {
             synchronized (ImfLock.class) {
-                final var bindingController = getInputMethodBindingController(mCurrentUserId);
+                final var bindingController = getInputMethodBindingController(userId);
+                final var userData = getUserData(userId);
                 // TODO(b/305829876): Implement user ID verification
-                if (mCurClient != null) {
-                    clearClientSessionForAccessibilityLocked(mCurClient, accessibilityConnectionId);
-                    mCurClient.mAccessibilitySessions.put(
+                if (userData.mCurClient != null) {
+                    clearClientSessionForAccessibilityLocked(userData.mCurClient,
+                            accessibilityConnectionId);
+                    userData.mCurClient.mAccessibilitySessions.put(
                             accessibilityConnectionId,
-                            new AccessibilitySessionState(mCurClient,
+                            new AccessibilitySessionState(userData.mCurClient,
                                     accessibilityConnectionId,
                                     session));
 
                     attachNewAccessibilityLocked(StartInputReason.SESSION_CREATED_BY_ACCESSIBILITY,
-                            true);
+                            true, userId);
 
-                    final SessionState sessionState = mCurClient.mCurSession;
+                    final SessionState sessionState = userData.mCurClient.mCurSession;
                     final IInputMethodSession imeSession = sessionState == null
                             ? null : sessionState.mSession;
                     final SparseArray<IAccessibilityInputMethodSession>
                             accessibilityInputMethodSessions =
                             createAccessibilityInputMethodSessions(
-                                    mCurClient.mAccessibilitySessions);
+                                    userData.mCurClient.mAccessibilitySessions);
                     final InputBindResult res = new InputBindResult(
                             InputBindResult.ResultCode.SUCCESS_WITH_ACCESSIBILITY_SESSION,
                             imeSession, accessibilityInputMethodSessions, /* channel= */ null,
                             bindingController.getCurId(),
                             bindingController.getSequenceNumber(),
                             /* isInputMethodSuppressingSpellChecker= */ false);
-                    mCurClient.mClient.onBindAccessibilityService(res, accessibilityConnectionId);
+                    userData.mCurClient.mClient.onBindAccessibilityService(res,
+                            accessibilityConnectionId);
                 }
             }
         }
@@ -5840,33 +5896,34 @@
         public void unbindAccessibilityFromCurrentClient(int accessibilityConnectionId,
                 @UserIdInt int userId) {
             synchronized (ImfLock.class) {
-                final var bindingController = getInputMethodBindingController(mCurrentUserId);
+                final var bindingController = getInputMethodBindingController(userId);
+                final var userData = getUserData(userId);
                 // TODO(b/305829876): Implement user ID verification
-                if (mCurClient != null) {
+                if (userData.mCurClient != null) {
                     if (DEBUG) {
                         Slog.v(TAG, "unbindAccessibilityFromCurrentClientLocked: client="
-                                + mCurClient.mClient.asBinder());
+                                + userData.mCurClient.mClient.asBinder());
                     }
                     // A11yManagerService unbinds the disabled accessibility service. We don't need
                     // to do it here.
-                    mCurClient.mClient.onUnbindAccessibilityService(
+                    userData.mCurClient.mClient.onUnbindAccessibilityService(
                             bindingController.getSequenceNumber(),
                             accessibilityConnectionId);
                 }
                 // We only have sessions when we bound to an input method. Remove this session
                 // from all clients.
-                if (getCurMethodLocked() != null) {
+                if (bindingController.getCurMethod() != null) {
                     // TODO(b/324907325): Remove the suppress warnings once b/324907325 is fixed.
                     @SuppressWarnings("GuardedBy") Consumer<ClientState> clearClientSession =
                             c -> clearClientSessionForAccessibilityLocked(c,
                                     accessibilityConnectionId);
                     mClientController.forAllClients(clearClientSession);
 
-                    AccessibilitySessionState session = mEnabledAccessibilitySessions.get(
+                    AccessibilitySessionState session = userData.mEnabledAccessibilitySessions.get(
                             accessibilityConnectionId);
                     if (session != null) {
                         finishSessionForAccessibilityLocked(session);
-                        mEnabledAccessibilitySessions.remove(accessibilityConnectionId);
+                        userData.mEnabledAccessibilitySessions.remove(accessibilityConnectionId);
                     }
                 }
             }
@@ -5883,14 +5940,15 @@
         public void onSwitchKeyboardLayoutShortcut(int direction, int displayId,
                 IBinder targetWindowToken) {
             synchronized (ImfLock.class) {
-                switchKeyboardLayoutLocked(direction);
+                // TODO(b/305849394): Infer userId from displayId
+                switchKeyboardLayoutLocked(direction, mCurrentUserId);
             }
         }
     }
 
     @BinderThread
     private IInputContentUriToken createInputContentUriToken(@Nullable IBinder token,
-            @Nullable Uri contentUri, @Nullable String packageName) {
+            @Nullable Uri contentUri, @Nullable String packageName, @UserIdInt int imeUserId) {
         if (token == null) {
             throw new NullPointerException("token");
         }
@@ -5907,15 +5965,6 @@
 
         synchronized (ImfLock.class) {
             final int uid = Binder.getCallingUid();
-            final int imeUserId = UserHandle.getUserId(uid);
-            if (imeUserId != mCurrentUserId) {
-                // Currently concurrent multi-user is not supported here due to the remaining
-                // dependency on mCurEditorInfo and mCurClient.
-                // TODO(b/341558132): Remove this early-exit once it becomes multi-user ready.
-                Slog.i(TAG, "Ignoring createInputContentUriToken due to user ID mismatch."
-                        + " imeUserId=" + imeUserId + " mCurrentUserId=" + mCurrentUserId);
-                return null;
-            }
             final var bindingController = getInputMethodBindingController(imeUserId);
             if (bindingController.getSelectedMethodId() == null) {
                 return null;
@@ -5928,16 +5977,16 @@
             // We cannot simply distinguish a bad IME that reports an arbitrary package name from
             // an unfortunate IME whose internal state is already obsolete due to the asynchronous
             // nature of our system.  Let's compare it with our internal record.
-            // TODO(b/341558132): Use "imeUserId" to query per-user "curEditorInfo"
-            final var curPackageName = mCurEditorInfo != null ? mCurEditorInfo.packageName : null;
+            final var userData = getUserData(imeUserId);
+            final var curPackageName = userData.mCurEditorInfo != null
+                    ? userData.mCurEditorInfo.packageName : null;
             if (!TextUtils.equals(curPackageName, packageName)) {
                 Slog.e(TAG, "Ignoring createInputContentUriToken mCurEditorInfo.packageName="
                         + curPackageName + " packageName=" + packageName);
                 return null;
             }
             // This user ID can never be spoofed.
-            // TODO(b/341558132): Use "imeUserId" to query per-user "curClient"
-            final int appUserId = UserHandle.getUserId(mCurClient.mUid);
+            final int appUserId = UserHandle.getUserId(userData.mCurClient.mUid);
             // This user ID may be invalid if "contentUri" embedded an invalid user ID.
             final int contentUriOwnerUserId = ContentProvider.getUserIdFromUri(contentUri,
                     imeUserId);
@@ -5954,14 +6003,16 @@
     }
 
     @BinderThread
-    private void reportFullscreenMode(@NonNull IBinder token, boolean fullscreen) {
+    private void reportFullscreenMode(@NonNull IBinder token, boolean fullscreen,
+            @UserIdInt int userId) {
         synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(token)) {
+            if (!calledWithValidTokenLocked(token, userId)) {
                 return;
             }
-            if (mCurClient != null && mCurClient.mClient != null) {
-                mInFullscreenMode = fullscreen;
-                mCurClient.mClient.reportFullscreenMode(fullscreen);
+            final var userData = getUserData(userId);
+            if (userData.mCurClient != null && userData.mCurClient.mClient != null) {
+                userData.mInFullscreenMode = fullscreen;
+                userData.mCurClient.mClient.reportFullscreenMode(fullscreen);
             }
         }
     }
@@ -6054,7 +6105,9 @@
         final Printer p = new PrintWriterPrinter(pw);
 
         synchronized (ImfLock.class) {
-            final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId);
+            final int userId = mCurrentUserId;
+            final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
+            final var userData = getUserData(userId);
             p.println("Current Input Method Manager state:");
             final List<InputMethodInfo> methodList = settings.getMethodList();
             int numImes = methodList.size();
@@ -6084,16 +6137,16 @@
             mClientController.forAllClients(clientControllerDump);
             final var bindingController = getInputMethodBindingController(mCurrentUserId);
             p.println("  mCurrentUserId=" + mCurrentUserId);
-            p.println("  mCurMethodId=" + getSelectedMethodIdLocked());
-            client = mCurClient;
+            p.println("  mCurMethodId=" + bindingController.getSelectedMethodId());
+            client = userData.mCurClient;
             p.println("  mCurClient=" + client + " mCurSeq="
                     + bindingController.getSequenceNumber());
             p.println("  mFocusedWindowPerceptible=" + mFocusedWindowPerceptible);
-            mImeBindingState.dump(/* prefix= */ "  ", p);
+            userData.mImeBindingState.dump(/* prefix= */ "  ", p);
 
             p.println("  mCurId=" + bindingController.getCurId()
                     + " mHaveConnection=" + bindingController.hasMainConnection()
-                    + " mBoundToMethod=" + mBoundToMethod + " mVisibleBound="
+                    + " mBoundToMethod=" + userData.mBoundToMethod + " mVisibleBound="
                     + bindingController.isVisibleBound());
 
             p.println("  mUserDataRepository=");
@@ -6109,15 +6162,15 @@
                     };
             mUserDataRepository.forAllUserData(userDataDump);
 
-            p.println("  mCurToken=" + getCurTokenLocked());
-            p.println("  mCurTokenDisplayId=" + getCurTokenDisplayIdLocked());
+            p.println("  mCurToken=" + bindingController.getCurToken());
+            p.println("  mCurTokenDisplayId=" + bindingController.getCurTokenDisplayId());
             p.println("  mCurHostInputToken=" + bindingController.getCurHostInputToken());
             p.println("  mCurIntent=" + bindingController.getCurIntent());
-            method = getCurMethodLocked();
-            p.println("  mCurMethod=" + getCurMethodLocked());
-            p.println("  mEnabledSession=" + mEnabledSession);
+            method = bindingController.getCurMethod();
+            p.println("  mCurMethod=" + method);
+            p.println("  mEnabledSession=" + userData.mEnabledSession);
             mVisibilityStateComputer.dump(pw, "  ");
-            p.println("  mInFullscreenMode=" + mInFullscreenMode);
+            p.println("  mInFullscreenMode=" + userData.mInFullscreenMode);
             p.println("  mSystemReady=" + mSystemReady + " mInteractive=" + mIsInteractive);
             p.println("  mExperimentalConcurrentMultiUserModeEnabled="
                     + mExperimentalConcurrentMultiUserModeEnabled);
@@ -6153,20 +6206,24 @@
         } else {
             p.println("No input method client.");
         }
-
-        if (mImeBindingState.mFocusedWindowClient != null
-                && client != mImeBindingState.mFocusedWindowClient) {
-            p.println(" ");
-            p.println("Warning: Current input method client doesn't match the last focused. "
-                    + "window.");
-            p.println("Dumping input method client in the last focused window just in case.");
-            p.println(" ");
-            pw.flush();
-            try {
-                TransferPipe.dumpAsync(
-                        mImeBindingState.mFocusedWindowClient.mClient.asBinder(), fd, args);
-            } catch (IOException | RemoteException e) {
-                p.println("Failed to dump input method client in focused window: " + e);
+        synchronized (ImfLock.class) {
+            final int userId = mCurrentUserId;
+            final var userData = getUserData(userId);
+            if (userData.mImeBindingState.mFocusedWindowClient != null
+                    && client != userData.mImeBindingState.mFocusedWindowClient) {
+                p.println(" ");
+                p.println("Warning: Current input method client doesn't match the last focused. "
+                        + "window.");
+                p.println("Dumping input method client in the last focused window just in case.");
+                p.println(" ");
+                pw.flush();
+                try {
+                    TransferPipe.dumpAsync(
+                            userData.mImeBindingState.mFocusedWindowClient.mClient.asBinder(), fd,
+                            args);
+                } catch (IOException | RemoteException e) {
+                    p.println("Failed to dump input method client in focused window: " + e);
+                }
             }
         }
 
@@ -6602,18 +6659,21 @@
                     final List<InputMethodInfo> nextEnabledImes;
                     final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
                     if (userId == mCurrentUserId) {
+                        final var userData = getUserData(userId);
                         if (Flags.refactorInsetsController()) {
-                            if (mImeBindingState != null
-                                    && mImeBindingState.mFocusedWindowClient != null
-                                    && mImeBindingState.mFocusedWindowClient.mClient != null) {
-                                mImeBindingState.mFocusedWindowClient.mClient.setImeVisibility(
-                                        false);
+                            if (userData.mImeBindingState != null
+                                    && userData.mImeBindingState.mFocusedWindowClient != null
+                                    && userData.mImeBindingState.mFocusedWindowClient.mClient
+                                    != null) {
+                                userData.mImeBindingState.mFocusedWindowClient.mClient
+                                        .setImeVisibility(false);
                             } else {
                                 // TODO(b329229469): ImeTracker?
                             }
                         } else {
-                            hideCurrentInputLocked(mImeBindingState.mFocusedWindow, 0 /* flags */,
-                                    SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND);
+                            hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow,
+                                    0 /* flags */,
+                                    SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND, userId);
                         }
                         final var bindingController = getInputMethodBindingController(userId);
                         bindingController.unbindCurrentMethod();
@@ -6633,7 +6693,7 @@
                         if (!chooseNewDefaultIMELocked()) {
                             resetSelectedInputMethodAndSubtypeLocked(null);
                         }
-                        updateInputMethodsFromSettingsLocked(true /* enabledMayChange */);
+                        updateInputMethodsFromSettingsLocked(true /* enabledMayChange */, userId);
                         InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
                                 getPackageManagerForUser(mContext, settings.getUserId()),
                                 settings.getEnabledInputMethodList());
@@ -6742,13 +6802,15 @@
      * @param reason the reason why the IME request was created
      */
     @NonNull
+    @GuardedBy("ImfLock.class")
     private ImeTracker.Token createStatsTokenForFocusedClient(boolean show,
-            @SoftInputShowHideReason int reason) {
-        final int uid = mImeBindingState.mFocusedWindowClient != null
-                ? mImeBindingState.mFocusedWindowClient.mUid
+            @SoftInputShowHideReason int reason, @UserIdInt int userId) {
+        final var userData = getUserData(userId);
+        final int uid = userData.mImeBindingState.mFocusedWindowClient != null
+                ? userData.mImeBindingState.mFocusedWindowClient.mUid
                 : -1;
-        final var packageName = mImeBindingState.mFocusedWindowEditorInfo != null
-                ? mImeBindingState.mFocusedWindowEditorInfo.packageName
+        final var packageName = userData.mImeBindingState.mFocusedWindowEditorInfo != null
+                ? userData.mImeBindingState.mFocusedWindowEditorInfo.packageName
                 : "uid(" + uid + ")";
 
         return ImeTracker.forLogging().onStart(packageName, uid,
@@ -6761,23 +6823,26 @@
         private final InputMethodManagerService mImms;
         @NonNull
         private final IBinder mToken;
+        @UserIdInt
+        private final int mUserId;
 
         InputMethodPrivilegedOperationsImpl(InputMethodManagerService imms,
-                @NonNull IBinder token) {
+                @NonNull IBinder token, @UserIdInt int userId) {
             mImms = imms;
             mToken = token;
+            mUserId = userId;
         }
 
         @BinderThread
         @Override
         public void setImeWindowStatusAsync(int vis, int backDisposition) {
-            mImms.setImeWindowStatus(mToken, vis, backDisposition);
+            mImms.setImeWindowStatus(mToken, vis, backDisposition, mUserId);
         }
 
         @BinderThread
         @Override
         public void reportStartInputAsync(IBinder startInputToken) {
-            mImms.reportStartInput(mToken, startInputToken);
+            mImms.reportStartInput(mToken, startInputToken, mUserId);
         }
 
         @BinderThread
@@ -6793,7 +6858,7 @@
             @SuppressWarnings("unchecked") final AndroidFuture<IBinder> typedFuture = future;
             try {
                 typedFuture.complete(mImms.createInputContentUriToken(
-                        mToken, contentUri, packageName).asBinder());
+                        mToken, contentUri, packageName, mUserId).asBinder());
             } catch (Throwable e) {
                 typedFuture.completeExceptionally(e);
             }
@@ -6802,7 +6867,7 @@
         @BinderThread
         @Override
         public void reportFullscreenModeAsync(boolean fullscreen) {
-            mImms.reportFullscreenMode(mToken, fullscreen);
+            mImms.reportFullscreenMode(mToken, fullscreen, mUserId);
         }
 
         @BinderThread
@@ -6810,7 +6875,7 @@
         public void setInputMethod(String id, AndroidFuture future /* T=Void */) {
             @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future;
             try {
-                mImms.setInputMethod(mToken, id);
+                mImms.setInputMethod(mToken, id, mUserId);
                 typedFuture.complete(null);
             } catch (Throwable e) {
                 typedFuture.completeExceptionally(e);
@@ -6823,7 +6888,7 @@
                 AndroidFuture future /* T=Void */) {
             @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future;
             try {
-                mImms.setInputMethodAndSubtype(mToken, id, subtype);
+                mImms.setInputMethodAndSubtype(mToken, id, subtype, mUserId);
                 typedFuture.complete(null);
             } catch (Throwable e) {
                 typedFuture.completeExceptionally(e);
@@ -6837,7 +6902,7 @@
                 AndroidFuture future /* T=Void */) {
             @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future;
             try {
-                mImms.hideMySoftInput(mToken, statsToken, flags, reason);
+                mImms.hideMySoftInput(mToken, statsToken, flags, reason, mUserId);
                 typedFuture.complete(null);
             } catch (Throwable e) {
                 typedFuture.completeExceptionally(e);
@@ -6851,7 +6916,7 @@
                 AndroidFuture future /* T=Void */) {
             @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future;
             try {
-                mImms.showMySoftInput(mToken, statsToken, flags, reason);
+                mImms.showMySoftInput(mToken, statsToken, flags, reason, mUserId);
                 typedFuture.complete(null);
             } catch (Throwable e) {
                 typedFuture.completeExceptionally(e);
@@ -6861,7 +6926,7 @@
         @BinderThread
         @Override
         public void updateStatusIconAsync(String packageName, @DrawableRes int iconId) {
-            mImms.updateStatusIcon(mToken, packageName, iconId);
+            mImms.updateStatusIcon(mToken, packageName, iconId, mUserId);
         }
 
         @BinderThread
@@ -6869,7 +6934,7 @@
         public void switchToPreviousInputMethod(AndroidFuture future /* T=Boolean */) {
             @SuppressWarnings("unchecked") final AndroidFuture<Boolean> typedFuture = future;
             try {
-                typedFuture.complete(mImms.switchToPreviousInputMethod(mToken));
+                typedFuture.complete(mImms.switchToPreviousInputMethod(mToken, mUserId));
             } catch (Throwable e) {
                 typedFuture.completeExceptionally(e);
             }
@@ -6881,7 +6946,8 @@
                 AndroidFuture future /* T=Boolean */) {
             @SuppressWarnings("unchecked") final AndroidFuture<Boolean> typedFuture = future;
             try {
-                typedFuture.complete(mImms.switchToNextInputMethod(mToken, onlyCurrentIme));
+                typedFuture.complete(mImms.switchToNextInputMethod(mToken, onlyCurrentIme,
+                        mUserId));
             } catch (Throwable e) {
                 typedFuture.completeExceptionally(e);
             }
@@ -6892,7 +6958,7 @@
         public void shouldOfferSwitchingToNextInputMethod(AndroidFuture future /* T=Boolean */) {
             @SuppressWarnings("unchecked") final AndroidFuture<Boolean> typedFuture = future;
             try {
-                typedFuture.complete(mImms.shouldOfferSwitchingToNextInputMethod(mToken));
+                typedFuture.complete(mImms.shouldOfferSwitchingToNextInputMethod(mToken, mUserId));
             } catch (Throwable e) {
                 typedFuture.completeExceptionally(e);
             }
@@ -6901,20 +6967,20 @@
         @BinderThread
         @Override
         public void notifyUserActionAsync() {
-            mImms.notifyUserAction(mToken);
+            mImms.notifyUserAction(mToken, mUserId);
         }
 
         @BinderThread
         @Override
         public void applyImeVisibilityAsync(IBinder windowToken, boolean setVisible,
                 @NonNull ImeTracker.Token statsToken) {
-            mImms.applyImeVisibility(mToken, windowToken, setVisible, statsToken);
+            mImms.applyImeVisibility(mToken, windowToken, setVisible, statsToken, mUserId);
         }
 
         @BinderThread
         @Override
         public void onStylusHandwritingReady(int requestId, int pid) {
-            mImms.onStylusHandwritingReady(requestId, pid);
+            mImms.onStylusHandwritingReady(requestId, pid, mUserId);
         }
 
         @BinderThread
@@ -6927,12 +6993,12 @@
         @Override
         public void switchKeyboardLayoutAsync(int direction) {
             synchronized (ImfLock.class) {
-                if (!mImms.calledWithValidTokenLocked(mToken)) {
+                if (!mImms.calledWithValidTokenLocked(mToken, mUserId)) {
                     return;
                 }
                 final long ident = Binder.clearCallingIdentity();
                 try {
-                    mImms.switchKeyboardLayoutLocked(direction);
+                    mImms.switchKeyboardLayoutLocked(direction, mUserId);
                 } finally {
                     Binder.restoreCallingIdentity(ident);
                 }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
index 326ef7e..89a31e7 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
@@ -79,6 +79,7 @@
         if (DEBUG) Slog.v(TAG, "Show switching menu. showAuxSubtypes=" + showAuxSubtypes);
 
         final int userId = mService.getCurrentImeUserIdLocked();
+        final var bindingController = mService.getInputMethodBindingController(userId);
 
         hideInputMethodMenuLocked();
 
@@ -86,9 +87,9 @@
             final InputMethodSubtype currentSubtype =
                     mService.getCurrentInputMethodSubtypeLocked();
             if (currentSubtype != null) {
-                final String curMethodId = mService.getSelectedMethodIdLocked();
+                final String curMethodId = bindingController.getSelectedMethodId();
                 final InputMethodInfo currentImi =
-                        mService.queryInputMethodForCurrentUserLocked(curMethodId);
+                        InputMethodSettingsRepository.get(userId).getMethodMap().get(curMethodId);
                 preferredInputMethodSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(
                         currentImi, currentSubtype.hashCode());
             }
@@ -179,7 +180,7 @@
                     if (subtypeId < 0 || subtypeId >= im.getSubtypeCount()) {
                         subtypeId = NOT_A_SUBTYPE_ID;
                     }
-                    mService.setInputMethodLocked(im.getId(), subtypeId);
+                    mService.setInputMethodLocked(im.getId(), subtypeId, userId);
                 }
                 hideInputMethodMenuLocked();
             }
diff --git a/services/core/java/com/android/server/inputmethod/UserDataRepository.java b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
index 3da4a14..48284fb 100644
--- a/services/core/java/com/android/server/inputmethod/UserDataRepository.java
+++ b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
@@ -17,12 +17,18 @@
 package com.android.server.inputmethod;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.content.pm.UserInfo;
 import android.os.Handler;
 import android.util.SparseArray;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ImeTracker;
+import android.window.ImeOnBackInvokedDispatcher;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
+import com.android.internal.inputmethod.IRemoteInputConnection;
 import com.android.server.pm.UserManagerInternal;
 
 import java.util.function.Consumer;
@@ -96,6 +102,78 @@
         final HardwareKeyboardShortcutController mHardwareKeyboardShortcutController;
 
         /**
+         * Have we called mCurMethod.bindInput()?
+         */
+        @GuardedBy("ImfLock.class")
+        boolean mBoundToMethod = false;
+
+        /**
+         * Have we called bindInput() for accessibility services?
+         */
+        @GuardedBy("ImfLock.class")
+        boolean mBoundToAccessibility;
+
+        @GuardedBy("ImfLock.class")
+        @NonNull
+        ImeBindingState mImeBindingState = ImeBindingState.newEmptyState();
+
+        @GuardedBy("ImfLock.class")
+        @Nullable
+        ClientState mCurClient = null;
+
+        @GuardedBy("ImfLock.class")
+        boolean mInFullscreenMode;
+
+        /**
+         * The {@link IRemoteInputConnection} last provided by the current client.
+         */
+        @GuardedBy("ImfLock.class")
+        @Nullable
+        IRemoteInputConnection mCurInputConnection;
+
+        /**
+         * The {@link ImeOnBackInvokedDispatcher} last provided by the current client to
+         * receive {@link android.window.OnBackInvokedCallback}s forwarded from IME.
+         */
+        @GuardedBy("ImfLock.class")
+        @Nullable
+        ImeOnBackInvokedDispatcher mCurImeDispatcher;
+
+        /**
+         * The {@link IRemoteAccessibilityInputConnection} last provided by the current client.
+         */
+        @GuardedBy("ImfLock.class")
+        @Nullable
+        IRemoteAccessibilityInputConnection mCurRemoteAccessibilityInputConnection;
+
+        /**
+         * The {@link EditorInfo} last provided by the current client.
+         */
+        @GuardedBy("ImfLock.class")
+        @Nullable
+        EditorInfo mCurEditorInfo;
+
+        /**
+         * The token tracking the current IME show request that is waiting for a connection to an
+         * IME, otherwise {@code null}.
+         */
+        @GuardedBy("ImfLock.class")
+        @Nullable
+        ImeTracker.Token mCurStatsToken;
+
+        /**
+         * Currently enabled session.
+         */
+        @GuardedBy("ImfLock.class")
+        @Nullable
+        InputMethodManagerService.SessionState mEnabledSession;
+
+        @GuardedBy("ImfLock.class")
+        @Nullable
+        SparseArray<InputMethodManagerService.AccessibilitySessionState>
+                mEnabledAccessibilitySessions = new SparseArray<>();
+
+        /**
          * Intended to be instantiated only from this file.
          */
         private UserData(@UserIdInt int userId,
diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
index 757c07c..41aac32 100644
--- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
+++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
@@ -254,7 +254,7 @@
                 synchronized (ImfLock.class) {
                     ClientState cs = imms.getClientStateLocked(client);
                     if (cs != null) {
-                        imms.requestClientSessionLocked(cs);
+                        imms.requestClientSessionLocked(cs, userId);
                         imms.requestClientSessionForAccessibilityLocked(cs);
                     }
                 }
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 381b667..a0aad52 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -225,19 +225,20 @@
         @Override
         public void handleNanoappMessage(short hostEndpointId, NanoAppMessage message,
                 List<String> nanoappPermissions, List<String> messagePermissions) {
-            if (Flags.reliableMessageImplementation()
+            // Only process the message normally if not using test mode manager or if
+            // the test mode manager call returned false as this indicates it did not
+            // process the message.
+            boolean useTestModeManager = Flags.reliableMessageImplementation()
                     && Flags.reliableMessageTestModeBehavior()
-                    && mIsTestModeEnabled.get()
-                    && mTestModeManager.handleNanoappMessage(() -> {
-                        handleClientMessageCallback(mContextHubId, hostEndpointId, message,
-                                nanoappPermissions, messagePermissions);
+                    && mIsTestModeEnabled.get();
+            if (!useTestModeManager
+                    || !mTestModeManager.handleNanoappMessage(() -> {
+                        handleClientMessageCallback(mContextHubId, hostEndpointId,
+                                message, nanoappPermissions, messagePermissions);
                     }, message)) {
-                // The ContextHubTestModeManager handled the nanoapp message, so return here.
-                return;
+                handleClientMessageCallback(mContextHubId, hostEndpointId,
+                        message, nanoappPermissions, messagePermissions);
             }
-
-            handleClientMessageCallback(mContextHubId, hostEndpointId, message,
-                    nanoappPermissions, messagePermissions);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 6eac72d..173fc5c 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -2504,13 +2504,13 @@
         Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
     }
 
-    private void enableRestrictedSettings(String pkgName, int appId, int userId) {
+    private void setAccessRestrictedSettingsMode(String pkgName, int appId, int userId, int mode) {
         final AppOpsManager appOpsManager = mPm.mContext.getSystemService(AppOpsManager.class);
         final int uid = UserHandle.getUid(userId, appId);
         appOpsManager.setMode(AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS,
                 uid,
                 pkgName,
-                AppOpsManager.MODE_ERRORED);
+                mode);
     }
 
     /**
@@ -2888,8 +2888,21 @@
                 mPm.notifyPackageChanged(packageName, request.getAppId());
             }
 
-            if (!android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()
-                    || !android.security.Flags.extendEcmToAllSettings()) {
+            // Set the OP_ACCESS_RESTRICTED_SETTINGS op, which is used by ECM (see {@link
+            // EnhancedConfirmationManager}) as a persistent state denoting whether an app is
+            // currently guarded by ECM, not guarded by ECM, or (in Android V+) that this should
+            // be decided later.
+            if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()
+                    && android.security.Flags.extendEcmToAllSettings()) {
+                final int appId = request.getAppId();
+                mPm.mHandler.post(() -> {
+                    for (int userId : firstUserIds) {
+                        // MODE_DEFAULT means that the app's guardedness will be decided lazily
+                        setAccessRestrictedSettingsMode(packageName, appId, userId,
+                                AppOpsManager.MODE_DEFAULT);
+                    }
+                });
+            } else {
                 // Apply restricted settings on potentially dangerous packages. Needs to happen
                 // after appOpsManager is notified of the new package
                 if (request.getPackageSource() == PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE
@@ -2898,7 +2911,9 @@
                     final int appId = request.getAppId();
                     mPm.mHandler.post(() -> {
                         for (int userId : firstUserIds) {
-                            enableRestrictedSettings(packageName, appId, userId);
+                            // MODE_ERRORED means that the app is explicitly guarded
+                            setAccessRestrictedSettingsMode(packageName, appId, userId,
+                                    AppOpsManager.MODE_ERRORED);
                         }
                     });
                 }
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index eabc979..1b7bf89 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -28,6 +28,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.app.ActivityManager;
 import android.app.AlarmManager;
 import android.app.usage.NetworkStatsManager;
@@ -1974,13 +1975,15 @@
         private WifiManager mWifiManager;
         private BluetoothPowerStatsCollector.BluetoothStatsRetriever mBluetoothStatsRetriever;
 
+        @SuppressLint("WifiManagerPotentialLeak")
         void setContext(Context context) {
             mPackageManager = context.getPackageManager();
             mConsumedEnergyRetriever = new PowerStatsCollector.ConsumedEnergyRetrieverImpl(
                     LocalServices.getService(PowerStatsInternal.class));
             mNetworkStatsManager = context.getSystemService(NetworkStatsManager.class);
-            mTelephonyManager = context.getSystemService(TelephonyManager.class);
-            mWifiManager = context.getSystemService(WifiManager.class);
+            mTelephonyManager =
+                    (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+            mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
             mBluetoothStatsRetriever = new BluetoothStatsRetrieverImpl(
                     context.getSystemService(BluetoothManager.class));
         }
@@ -11288,10 +11291,11 @@
         mCpuPowerStatsCollector.addConsumer(this::recordPowerStats);
 
         mMobileRadioPowerStatsCollector = new MobileRadioPowerStatsCollector(
-                mPowerStatsCollectorInjector);
+                mPowerStatsCollectorInjector, this::onMobileRadioPowerStatsRetrieved);
         mMobileRadioPowerStatsCollector.addConsumer(this::recordPowerStats);
 
-        mWifiPowerStatsCollector = new WifiPowerStatsCollector(mPowerStatsCollectorInjector);
+        mWifiPowerStatsCollector = new WifiPowerStatsCollector(mPowerStatsCollectorInjector,
+                this::onWifiPowerStatsRetrieved);
         mWifiPowerStatsCollector.addConsumer(this::recordPowerStats);
 
         mBluetoothPowerStatsCollector = new BluetoothPowerStatsCollector(
@@ -12320,16 +12324,13 @@
 
     /**
      * Distribute WiFi energy info and network traffic to apps.
+     *
      * @param info The energy information from the WiFi controller.
      */
     @GuardedBy("this")
     public void updateWifiState(@Nullable final WifiActivityEnergyInfo info,
             final long consumedChargeUC, long elapsedRealtimeMs, long uptimeMs,
             @NonNull NetworkStatsManager networkStatsManager) {
-        if (mWifiPowerStatsCollector.isEnabled()) {
-            return;
-        }
-
         if (DEBUG_ENERGY) {
             synchronized (mWifiNetworkLock) {
                 Slog.d(TAG, "Updating wifi stats: " + Arrays.toString(mWifiIfaces));
@@ -12347,7 +12348,20 @@
                 delta = null;
             }
         }
+        updateWifiBatteryStats(info, delta, consumedChargeUC, elapsedRealtimeMs, uptimeMs);
+    }
 
+    private void onWifiPowerStatsRetrieved(WifiActivityEnergyInfo wifiActivityEnergyInfo,
+            List<NetworkStatsDelta> networkStatsDeltas, long elapsedRealtimeMs, long uptimeMs) {
+        // Do not populate consumed energy, because energy attribution is done by
+        // WifiPowerStatsProcessor.
+        updateWifiBatteryStats(wifiActivityEnergyInfo, networkStatsDeltas, POWER_DATA_UNAVAILABLE,
+                elapsedRealtimeMs, uptimeMs);
+    }
+
+    private void updateWifiBatteryStats(WifiActivityEnergyInfo info,
+            List<NetworkStatsDelta> delta, long consumedChargeUC, long elapsedRealtimeMs,
+            long uptimeMs) {
         synchronized (this) {
             if (!mOnBatteryInternal || mIgnoreNextExternalStats) {
                 if (mIgnoreNextExternalStats) {
@@ -12711,9 +12725,6 @@
                 : mLastModemActivityInfo.getDelta(activityInfo);
         mLastModemActivityInfo = activityInfo;
 
-        // Add modem tx power to history.
-        addModemTxPowerToHistory(deltaInfo, elapsedRealtimeMs, uptimeMs);
-
         // Grab a separate lock to acquire the network stats, which may do I/O.
         List<NetworkStatsDelta> delta = null;
         synchronized (mModemNetworkLock) {
@@ -12724,6 +12735,23 @@
             }
         }
 
+        updateCellularBatteryStats(deltaInfo, delta, consumedChargeUC, elapsedRealtimeMs, uptimeMs);
+    }
+
+    private void onMobileRadioPowerStatsRetrieved(ModemActivityInfo modemActivityInfo,
+            List<NetworkStatsDelta> networkStatsDeltas, long elapsedRealtimeMs, long uptimeMs) {
+        // Do not populate consumed energy, because energy attribution is done by
+        // MobileRadioPowerStatsProcessor.
+        updateCellularBatteryStats(modemActivityInfo, networkStatsDeltas, POWER_DATA_UNAVAILABLE,
+                elapsedRealtimeMs, uptimeMs);
+    }
+
+    private void updateCellularBatteryStats(@Nullable ModemActivityInfo deltaInfo,
+            @Nullable List<NetworkStatsDelta> delta, long consumedChargeUC, long elapsedRealtimeMs,
+            long uptimeMs) {
+        // Add modem tx power to history.
+        addModemTxPowerToHistory(deltaInfo, elapsedRealtimeMs, uptimeMs);
+
         synchronized (this) {
             final long totalRadioDurationMs =
                     mMobileRadioActiveTimer.getTimeSinceMarkLocked(
@@ -13111,7 +13139,6 @@
 
                 final long rxTimeMs = deltaInfo.getReceiveTimeMillis(rat, freq);
                 final int[] txTimesMs = deltaInfo.getTransmitTimeMillis(rat, freq);
-
                 ratStats.incrementRxDuration(freq, rxTimeMs);
                 if (isMobileRadioEnergyConsumerSupportedLocked()) {
                     // Accumulate the power cost of time spent receiving in a particular state.
@@ -15387,16 +15414,15 @@
     /*@hide */
     public WifiBatteryStats getWifiBatteryStats() {
         final int which = STATS_SINCE_CHARGED;
-        final long rawRealTimeUs = SystemClock.elapsedRealtime() * 1000;
+        final long rawRealTimeUs = mClock.elapsedRealtime() * 1000;
         final ControllerActivityCounter counter = getWifiControllerActivity();
         final long idleTimeMs = counter.getIdleTimeCounter().getCountLocked(which);
         final long scanTimeMs = counter.getScanTimeCounter().getCountLocked(which);
         final long rxTimeMs = counter.getRxTimeCounter().getCountLocked(which);
         final long txTimeMs = counter.getTxTimeCounters()[0].getCountLocked(which);
-        final long totalControllerActivityTimeMs
-                = computeBatteryRealtime(SystemClock.elapsedRealtime() * 1000, which) / 1000;
-        final long sleepTimeMs
-                = totalControllerActivityTimeMs - (idleTimeMs + rxTimeMs + txTimeMs);
+        final long totalControllerActivityTimeMs =
+                computeBatteryRealtime(mClock.elapsedRealtime() * 1000, which) / 1000;
+        final long sleepTimeMs = totalControllerActivityTimeMs - (idleTimeMs + rxTimeMs + txTimeMs);
         final long energyConsumedMaMs = counter.getPowerCounter().getCountLocked(which);
         final long monitoredRailChargeConsumedMaMs =
                 counter.getMonitoredRailChargeConsumedMaMs().getCountLocked(which);
diff --git a/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsCollector.java
index 33ea563..c88e1b0 100644
--- a/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsCollector.java
@@ -16,6 +16,7 @@
 
 package com.android.server.power.stats;
 
+import android.annotation.Nullable;
 import android.content.pm.PackageManager;
 import android.hardware.power.stats.EnergyConsumerType;
 import android.net.NetworkStats;
@@ -69,6 +70,13 @@
             AccessNetworkConstants.AccessNetworkType.NGRAN
     };
 
+    interface Observer {
+        void onMobileRadioPowerStatsRetrieved(
+                @Nullable ModemActivityInfo modemActivityDelta,
+                @Nullable List<BatteryStatsImpl.NetworkStatsDelta> networkStatsDeltas,
+                long elapsedRealtimeMs, long uptimeMs);
+    }
+
     interface Injector {
         Handler getHandler();
         Clock getClock();
@@ -84,6 +92,7 @@
     }
 
     private final Injector mInjector;
+    private final Observer mObserver;
 
     private MobileRadioPowerStatsLayout mLayout;
     private boolean mIsInitialized;
@@ -105,13 +114,14 @@
     private long mLastCallDuration;
     private long mLastScanDuration;
 
-    MobileRadioPowerStatsCollector(Injector injector) {
+    MobileRadioPowerStatsCollector(Injector injector, Observer observer) {
         super(injector.getHandler(), injector.getPowerStatsCollectionThrottlePeriod(
                         BatteryConsumer.powerComponentIdToString(
                                 BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO)),
                 injector.getUidResolver(),
                 injector.getClock());
         mInjector = injector;
+        mObserver = observer;
     }
 
     @Override
@@ -198,10 +208,8 @@
         Arrays.fill(mPowerStats.stats, 0);
         mPowerStats.uidStats.clear();
 
-        collectModemActivityInfo();
-
-        collectNetworkStats();
-
+        ModemActivityInfo modemActivityDelta = collectModemActivityInfo();
+        List<BatteryStatsImpl.NetworkStatsDelta> networkStatsDeltas = collectNetworkStats();
         if (mEnergyConsumerIds.length != 0) {
             collectEnergyConsumers();
         }
@@ -210,12 +218,16 @@
             setTimestamp(mClock.elapsedRealtime());
         }
 
+        if (mObserver != null) {
+            mObserver.onMobileRadioPowerStatsRetrieved(modemActivityDelta,
+                    networkStatsDeltas, mClock.elapsedRealtime(), mClock.uptimeMillis());
+        }
         return mPowerStats;
     }
 
-    private void collectModemActivityInfo() {
+    private ModemActivityInfo collectModemActivityInfo() {
         if (mTelephonyManager == null) {
-            return;
+            return null;
         }
 
         CompletableFuture<ModemActivityInfo> immediateFuture = new CompletableFuture<>();
@@ -243,7 +255,7 @@
         }
 
         if (activityInfo == null) {
-            return;
+            return null;
         }
 
         ModemActivityInfo deltaInfo = mLastModemActivityInfo == null
@@ -293,12 +305,13 @@
                 }
             }
         }
+        return deltaInfo;
     }
 
-    private void collectNetworkStats() {
+    private List<BatteryStatsImpl.NetworkStatsDelta> collectNetworkStats() {
         NetworkStats networkStats = mNetworkStatsSupplier.get();
         if (networkStats == null) {
-            return;
+            return null;
         }
 
         List<BatteryStatsImpl.NetworkStatsDelta> delta =
@@ -330,6 +343,7 @@
                 mLayout.setUidTxPackets(stats, mLayout.getUidTxPackets(stats) + txPackets);
             }
         }
+        return delta;
     }
 
     private void collectEnergyConsumers() {
diff --git a/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
index bd04199..6d519ee 100644
--- a/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
@@ -43,6 +43,12 @@
 
     private static final long ENERGY_UNSPECIFIED = -1;
 
+    interface Observer {
+        void onWifiPowerStatsRetrieved(WifiActivityEnergyInfo info,
+                List<BatteryStatsImpl.NetworkStatsDelta> delta, long elapsedRealtimeMs,
+                long uptimeMs);
+    }
+
     interface WifiStatsRetriever {
         interface Callback {
             void onWifiScanTime(int uid, long scanTimeMs, long batchScanTimeMs);
@@ -66,6 +72,7 @@
     }
 
     private final Injector mInjector;
+    private final Observer mObserver;
 
     private WifiPowerStatsLayout mLayout;
     private boolean mIsInitialized;
@@ -93,12 +100,13 @@
     private final SparseArray<WifiScanTimes> mLastScanTimes = new SparseArray<>();
     private long mLastWifiActiveDuration;
 
-    WifiPowerStatsCollector(Injector injector) {
+    WifiPowerStatsCollector(Injector injector, Observer observer) {
         super(injector.getHandler(), injector.getPowerStatsCollectionThrottlePeriod(
                         BatteryConsumer.powerComponentIdToString(
                                 BatteryConsumer.POWER_COMPONENT_WIFI)),
                 injector.getUidResolver(), injector.getClock());
         mInjector = injector;
+        mObserver = observer;
     }
 
     @Override
@@ -160,22 +168,27 @@
             return null;
         }
 
+        WifiActivityEnergyInfo activityInfo = null;
         if (mPowerReportingSupported) {
-            collectWifiActivityInfo();
+            activityInfo = collectWifiActivityInfo();
         } else {
             collectWifiActivityStats();
         }
-        collectNetworkStats();
+        List<BatteryStatsImpl.NetworkStatsDelta> networkStatsDeltas = collectNetworkStats();
         collectWifiScanTime();
 
         if (mEnergyConsumerIds.length != 0) {
             collectEnergyConsumers();
         }
 
+        if (mObserver != null) {
+            mObserver.onWifiPowerStatsRetrieved(activityInfo, networkStatsDeltas,
+                    mClock.elapsedRealtime(), mClock.uptimeMillis());
+        }
         return mPowerStats;
     }
 
-    private void collectWifiActivityInfo() {
+    private WifiActivityEnergyInfo collectWifiActivityInfo() {
         CompletableFuture<WifiActivityEnergyInfo> immediateFuture = new CompletableFuture<>();
         mWifiManager.getWifiActivityEnergyInfoAsync(Runnable::run,
                 immediateFuture::complete);
@@ -190,7 +203,7 @@
         }
 
         if (activityInfo == null) {
-            return;
+            return null;
         }
 
         long rxDuration = activityInfo.getControllerRxDurationMillis()
@@ -210,6 +223,9 @@
         mPowerStats.durationMs = rxDuration + txDuration + scanDuration + idleDuration;
 
         mLastWifiActivityInfo = activityInfo;
+
+        return new WifiActivityEnergyInfo(activityInfo.getTimeSinceBootMillis(),
+                activityInfo.getStackState(), txDuration, rxDuration, scanDuration, idleDuration);
     }
 
     private void collectWifiActivityStats() {
@@ -219,12 +235,12 @@
         mPowerStats.durationMs = duration;
     }
 
-    private void collectNetworkStats() {
+    private List<BatteryStatsImpl.NetworkStatsDelta> collectNetworkStats() {
         mPowerStats.uidStats.clear();
 
         NetworkStats networkStats = mNetworkStatsSupplier.get();
         if (networkStats == null) {
-            return;
+            return null;
         }
 
         List<BatteryStatsImpl.NetworkStatsDelta> delta =
@@ -256,6 +272,7 @@
                 mLayout.setUidTxPackets(stats, mLayout.getUidTxPackets(stats) + txPackets);
             }
         }
+        return delta;
     }
 
     private void collectWifiScanTime() {
diff --git a/services/core/java/com/android/server/stats/pull/BatteryHealthUtility.java b/services/core/java/com/android/server/stats/pull/BatteryHealthUtility.java
new file mode 100644
index 0000000..e0768fe
--- /dev/null
+++ b/services/core/java/com/android/server/stats/pull/BatteryHealthUtility.java
@@ -0,0 +1,89 @@
+/*
+ * 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.server.stats.pull;
+
+import android.util.StatsEvent;
+
+import com.android.internal.util.FrameworkStatsLog;
+
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Locale;
+
+/**
+ * Utility class to redact Battery Health data from HealthServiceWrapper
+ *
+ * @hide
+ */
+public abstract class BatteryHealthUtility {
+    /**
+     * Create a StatsEvent corresponding to the Battery Health data, the fields
+     * of which are redacted to preserve users' privacy.
+     * The redaction consists in truncating the timestamps to the Monday of the
+     * corresponding week, and reducing the battery serial into the last byte
+     * of its MD5.
+     */
+    public static StatsEvent buildStatsEvent(int atomTag,
+            android.hardware.health.BatteryHealthData data, int chargeStatus, int chargePolicy)
+            throws NoSuchAlgorithmException {
+        int manufacturingDate = secondsToWeekYYYYMMDD(data.batteryManufacturingDateSeconds);
+        int firstUsageDate = secondsToWeekYYYYMMDD(data.batteryFirstUsageSeconds);
+        long stateOfHealth = data.batteryStateOfHealth;
+        int partStatus = data.batteryPartStatus;
+        int serialHashTruncated = stringToIntHash(data.batterySerialNumber) & 0xFF; // Last byte
+
+        return FrameworkStatsLog.buildStatsEvent(atomTag, manufacturingDate, firstUsageDate,
+                (int) stateOfHealth, serialHashTruncated, partStatus, chargeStatus, chargePolicy);
+    }
+
+    private static int secondsToWeekYYYYMMDD(long seconds) {
+        Calendar calendar = Calendar.getInstance();
+        long millis = seconds * 1000L;
+
+        calendar.setTimeInMillis(millis);
+
+        // Truncate all date information, up to week, which is rounded to
+        // MONDAY
+        calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        calendar.set(Calendar.MILLISECOND, 0);
+
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd", Locale.US);
+
+        String formattedDate = sdf.format(calendar.getTime());
+
+        return Integer.parseInt(formattedDate);
+    }
+
+    private static int stringToIntHash(String data) throws NoSuchAlgorithmException {
+        if (data == null || data.isEmpty()) {
+            return 0;
+        }
+
+        MessageDigest digest = MessageDigest.getInstance("MD5");
+        byte[] hashBytes = digest.digest(data.getBytes());
+
+        // Convert to integer (simplest way, but potential for loss of information)
+        BigInteger bigInt = new BigInteger(1, hashBytes);
+        return bigInt.intValue();
+    }
+}
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index c1b825b..0041d39 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -119,6 +119,8 @@
 import android.net.NetworkTemplate;
 import android.net.wifi.WifiManager;
 import android.os.AsyncTask;
+import android.os.BatteryManager;
+import android.os.BatteryProperty;
 import android.os.BatteryStats;
 import android.os.BatteryStatsInternal;
 import android.os.BatteryStatsManager;
@@ -243,6 +245,7 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
@@ -769,6 +772,7 @@
                     case FrameworkStatsLog.FULL_BATTERY_CAPACITY:
                     case FrameworkStatsLog.BATTERY_VOLTAGE:
                     case FrameworkStatsLog.BATTERY_CYCLE_COUNT:
+                    case FrameworkStatsLog.BATTERY_HEALTH:
                         synchronized (mHealthHalLock) {
                             return pullHealthHalLocked(atomTag, data);
                         }
@@ -999,6 +1003,7 @@
         registerFullBatteryCapacity();
         registerBatteryVoltage();
         registerBatteryCycleCount();
+        registerBatteryHealth();
         registerSettingsStats();
         registerInstalledIncrementalPackages();
         registerKeystoreStorageStats();
@@ -4365,7 +4370,15 @@
         );
     }
 
-    int pullHealthHalLocked(int atomTag, List<StatsEvent> pulledData) {
+    private void registerBatteryHealth() {
+        int tagId = FrameworkStatsLog.BATTERY_HEALTH;
+        mStatsManager.setPullAtomCallback(tagId,
+                null, // use default PullAtomMetadata values
+                DIRECT_EXECUTOR, mStatsCallbackImpl);
+    }
+
+    @GuardedBy("mHealthHalLock")
+    private int pullHealthHalLocked(int atomTag, List<StatsEvent> pulledData) {
         if (mHealthService == null) {
             return StatsManager.PULL_SKIP;
         }
@@ -4396,6 +4409,44 @@
             case FrameworkStatsLog.BATTERY_CYCLE_COUNT:
                 pulledValue = healthInfo.batteryCycleCount;
                 break;
+            case FrameworkStatsLog.BATTERY_HEALTH:
+                android.hardware.health.BatteryHealthData bhd;
+                try {
+                    bhd = mHealthService.getBatteryHealthData();
+                } catch (RemoteException | IllegalStateException e) {
+                    return StatsManager.PULL_SKIP;
+                }
+                if (bhd == null) {
+                    return StatsManager.PULL_SKIP;
+                }
+
+                StatsEvent batteryHealthEvent;
+                try {
+                    BatteryProperty chargeStatusProperty = new BatteryProperty();
+                    BatteryProperty chargePolicyProperty = new BatteryProperty();
+
+                    if (0 > mHealthService.getProperty(
+                                BatteryManager.BATTERY_PROPERTY_STATUS, chargeStatusProperty)) {
+                        return StatsManager.PULL_SKIP;
+                    }
+                    if (0 > mHealthService.getProperty(
+                                BatteryManager.BATTERY_PROPERTY_CHARGING_POLICY,
+                                chargePolicyProperty)) {
+                        return StatsManager.PULL_SKIP;
+                    }
+                    int chargeStatus = (int) chargeStatusProperty.getLong();
+                    int chargePolicy = (int) chargePolicyProperty.getLong();
+                    batteryHealthEvent = BatteryHealthUtility.buildStatsEvent(
+                            atomTag, bhd, chargeStatus, chargePolicy);
+                    pulledData.add(batteryHealthEvent);
+
+                    return StatsManager.PULL_SUCCESS;
+                } catch (RemoteException | IllegalStateException e) {
+                    Slog.e(TAG, "Failed to add pulled data", e);
+                } catch (NoSuchAlgorithmException e) {
+                    Slog.e(TAG, "Could not find message digest algorithm", e);
+                }
+                return StatsManager.PULL_SKIP;
             default:
                 return StatsManager.PULL_SKIP;
         }
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index b846947..72c7be3 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -101,6 +101,7 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.os.storage.StorageManager;
 import android.service.wallpaper.IWallpaperConnection;
 import android.service.wallpaper.IWallpaperEngine;
 import android.service.wallpaper.IWallpaperService;
@@ -2209,8 +2210,12 @@
     public ParcelFileDescriptor getWallpaperWithFeature(String callingPkg, String callingFeatureId,
             IWallpaperManagerCallback cb, final int which, Bundle outParams, int wallpaperUserId,
             boolean getCropped) {
-        final boolean hasPrivilege = hasPermission(READ_WALLPAPER_INTERNAL);
-        if (!hasPrivilege) checkPermission(MANAGE_EXTERNAL_STORAGE);
+        final boolean hasPrivilege = hasPermission(READ_WALLPAPER_INTERNAL)
+                || hasPermission(MANAGE_EXTERNAL_STORAGE);
+        if (!hasPrivilege) {
+            mContext.getSystemService(StorageManager.class).checkPermissionReadImages(true,
+                    Binder.getCallingPid(), Binder.getCallingUid(), callingPkg, callingFeatureId);
+        }
 
         wallpaperUserId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
                 Binder.getCallingUid(), wallpaperUserId, false, true, "getWallpaper", null);
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 6b25c84..dce496d 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -4578,9 +4578,6 @@
         if (!delayed) {
             updateReportedVisibilityLocked();
         }
-
-        // Reset the last saved PiP snap fraction on removal.
-        mDisplayContent.mPinnedTaskController.onActivityHidden(mActivityComponent);
         mDisplayContent.onRunningActivityChanged();
         mRemovingFromDisplay = false;
     }
@@ -6744,8 +6741,6 @@
         if (task.mLastRecentsAnimationTransaction != null) {
             task.clearLastRecentsAnimationTransaction(true /* forceRemoveOverlay */);
         }
-        // Reset the last saved PiP snap fraction on app stop.
-        mDisplayContent.mPinnedTaskController.onActivityHidden(mActivityComponent);
         if (isClientVisible()) {
             // Though this is usually unlikely to happen, still make sure the client is invisible.
             setClientVisible(false);
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
index c79565a..8bd8098 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.os.UserHandle.USER_SYSTEM;
 import static android.view.Display.TYPE_VIRTUAL;
 import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
 import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
@@ -27,6 +28,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.app.WindowConfiguration;
 import android.os.Environment;
 import android.util.ArrayMap;
@@ -42,6 +44,7 @@
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.wm.DisplayWindowSettings.SettingsProvider;
+import com.android.window.flags.Flags;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -53,6 +56,7 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Implementation of {@link SettingsProvider} that reads the base settings provided in a display
@@ -91,11 +95,11 @@
     @NonNull
     private ReadableSettings mBaseSettings;
     @NonNull
-    private final WritableSettings mOverrideSettings;
+    private WritableSettings mOverrideSettings;
 
     DisplayWindowSettingsProvider() {
         this(new AtomicFileStorage(getVendorSettingsFile()),
-                new AtomicFileStorage(getOverrideSettingsFile()));
+                new AtomicFileStorage(getOverrideSettingsFileForUser(USER_SYSTEM)));
     }
 
     @VisibleForTesting
@@ -133,6 +137,48 @@
         mBaseSettings = new ReadableSettings(baseSettingsStorage);
     }
 
+    /**
+     * Overrides the storage that should be used to save override settings for a user.
+     *
+     * @see #DATA_DISPLAY_SETTINGS_FILE_PATH
+     */
+    void setOverrideSettingsForUser(@UserIdInt int userId) {
+        if (!Flags.perUserDisplayWindowSettings()) {
+            return;
+        }
+        final AtomicFile settingsFile = getOverrideSettingsFileForUser(userId);
+        setOverrideSettingsStorage(new AtomicFileStorage(settingsFile));
+    }
+
+    /**
+     * Removes display override settings that are no longer associated with active displays.
+     * This is necessary because displays can be dynamically added or removed during
+     * the system's lifecycle (e.g., user switch, system server restart).
+     *
+     * @param root The root window container used to obtain the currently active displays.
+     */
+    void removeStaleDisplaySettings(@NonNull RootWindowContainer root) {
+        if (!Flags.perUserDisplayWindowSettings()) {
+            return;
+        }
+        final Set<String> displayIdentifiers = new ArraySet<>();
+        root.forAllDisplays(dc -> {
+            final String identifier = mOverrideSettings.getIdentifier(dc.getDisplayInfo());
+            displayIdentifiers.add(identifier);
+        });
+        mOverrideSettings.removeStaleDisplaySettings(displayIdentifiers);
+    }
+
+    /**
+     * Overrides the storage that should be used to save override settings.
+     *
+     * @see #setOverrideSettingsForUser(int)
+     */
+    @VisibleForTesting
+    void setOverrideSettingsStorage(@NonNull WritableSettingsStorage overrideSettingsStorage) {
+        mOverrideSettings = new WritableSettings(overrideSettingsStorage);
+    }
+
     @Override
     @NonNull
     public SettingsEntry getSettings(@NonNull DisplayInfo info) {
@@ -302,6 +348,12 @@
             mVirtualDisplayIdentifiers.remove(identifier);
         }
 
+        void removeStaleDisplaySettings(@NonNull Set<String> currentDisplayIdentifiers) {
+            if (mSettings.retainAll(currentDisplayIdentifiers)) {
+                writeSettings();
+            }
+        }
+
         private void writeSettings() {
             final FileData fileData = new FileData();
             fileData.mIdentifierType = mIdentifierType;
@@ -332,9 +384,14 @@
     }
 
     @NonNull
-    private static AtomicFile getOverrideSettingsFile() {
-        final File overrideSettingsFile = new File(Environment.getDataDirectory(),
-                DATA_DISPLAY_SETTINGS_FILE_PATH);
+    private static AtomicFile getOverrideSettingsFileForUser(@UserIdInt int userId) {
+        final File directory;
+        if (userId == USER_SYSTEM || !Flags.perUserDisplayWindowSettings()) {
+            directory = Environment.getDataDirectory();
+        } else {
+            directory = Environment.getDataSystemCeDirectory(userId);
+        }
+        final File overrideSettingsFile = new File(directory, DATA_DISPLAY_SETTINGS_FILE_PATH);
         return new AtomicFile(overrideSettingsFile, WM_DISPLAY_COMMIT_TAG);
     }
 
diff --git a/services/core/java/com/android/server/wm/PinnedTaskController.java b/services/core/java/com/android/server/wm/PinnedTaskController.java
index 4378b4f..755d4c8 100644
--- a/services/core/java/com/android/server/wm/PinnedTaskController.java
+++ b/services/core/java/com/android/server/wm/PinnedTaskController.java
@@ -22,7 +22,6 @@
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
 import android.app.PictureInPictureParams;
-import android.content.ComponentName;
 import android.content.res.Resources;
 import android.graphics.Insets;
 import android.graphics.Matrix;
@@ -326,19 +325,6 @@
     }
 
     /**
-     * Activity is hidden (either stopped or removed), resets the last saved snap fraction
-     * so that the default bounds will be returned for the next session.
-     */
-    void onActivityHidden(ComponentName componentName) {
-        if (mPinnedTaskListener == null) return;
-        try {
-            mPinnedTaskListener.onActivityHidden(componentName);
-        } catch (RemoteException e) {
-            Slog.e(TAG_WM, "Error delivering reset reentry fraction event.", e);
-        }
-    }
-
-    /**
      * Sets the Ime state and height.
      */
     void setAdjustedForIme(boolean adjustedForIme, int imeHeight) {
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 6abd488..76fd57b 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2158,6 +2158,12 @@
                 // Use Task#setBoundsUnchecked to skip checking windowing mode as the windowing mode
                 // will be updated later after this is collected in transition.
                 rootTask.setBoundsUnchecked(taskFragment.getBounds());
+                // The exit-PIP activity resumes early for seamless transition. In certain
+                // scenarios, this introduces unintended addition to recents. To address this,
+                // we mark the root task for automatic removal from recents. This ensures that
+                // after the pinned activity reparents to its original task, the root task is
+                // automatically removed from the recents list.
+                rootTask.autoRemoveRecents = true;
 
                 // Move the last recents animation transaction from original task to the new one.
                 if (task.mLastRecentsAnimationTransaction != null) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 5215609..6e0c16d0 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -3743,6 +3743,8 @@
                         null /* trigger */, null /* remote */, null /* disp */);
             }
             mCurrentUserId = newUserId;
+            mDisplayWindowSettingsProvider.setOverrideSettingsForUser(newUserId);
+            mDisplayWindowSettingsProvider.removeStaleDisplaySettings(mRoot);
             mPolicy.setCurrentUserLw(newUserId);
             mKeyguardDisableHandler.setCurrentUser(newUserId);
 
@@ -5479,6 +5481,9 @@
             // DisplayWindowSettings are applied. In addition, wide-color/hdr/isTouchDevice also
             // affect the Configuration.
             mRoot.forAllDisplays(DisplayContent::reconfigureDisplayLocked);
+            // Per-user display settings may leave outdated settings after user switches, especially
+            // during reboots starting with the default user without setCurrentUser called.
+            mDisplayWindowSettingsProvider.removeStaleDisplaySettings(mRoot);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index d26df7a..a49048c 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1105,6 +1105,7 @@
                     break;
                 }
                 if (activity.isVisible() || activity.isVisibleRequested()) {
+                    effects |= TRANSACT_EFFECTS_LIFECYCLE;
                     // Prevent the transition from being executed too early if the activity is
                     // visible.
                     activity.finishIfPossible("finish-activity-op", false /* oomAdj */);
@@ -1122,6 +1123,7 @@
                 launchOpts.remove(WindowContainerTransaction.HierarchyOp.LAUNCH_KEY_TASK_ID);
                 final SafeActivityOptions safeOptions =
                         SafeActivityOptions.fromBundle(launchOpts, caller.mPid, caller.mUid);
+                effects |= TRANSACT_EFFECTS_LIFECYCLE;
                 waitAsyncStart(() -> mService.mTaskSupervisor.startActivityFromRecents(
                         caller.mPid, caller.mUid, taskId, safeOptions));
                 break;
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index d4adba2..33ea9b4 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -50,6 +50,7 @@
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.internal.inputmethod.InputBindResult;
+import com.android.internal.inputmethod.SoftInputShowHideReason;
 import com.android.internal.inputmethod.StartInputFlags;
 import com.android.internal.inputmethod.StartInputReason;
 
@@ -73,9 +74,8 @@
     @Before
     public void setUp() throws RemoteException {
         super.setUp();
-        mVisibilityApplier =
-                (DefaultImeVisibilityApplier) mInputMethodManagerService.getVisibilityApplier();
         synchronized (ImfLock.class) {
+            mVisibilityApplier = mInputMethodManagerService.getVisibilityApplierLocked();
             mUserId = mInputMethodManagerService.getCurrentImeUserIdLocked();
             mInputMethodManagerService.setAttachedClientForTesting(requireNonNull(
                     mInputMethodManagerService.getClientStateLocked(mMockInputMethodClient)));
@@ -106,7 +106,7 @@
         assertThrows(IllegalArgumentException.class, () -> {
             synchronized (ImfLock.class) {
                 mVisibilityApplier.applyImeVisibility(mWindowToken, ImeTracker.Token.empty(),
-                        STATE_INVALID, mUserId);
+                        STATE_INVALID, eq(SoftInputShowHideReason.NOT_SET), mUserId);
             }
         });
     }
@@ -116,7 +116,7 @@
         final var statsToken = ImeTracker.Token.empty();
         synchronized (ImfLock.class) {
             mVisibilityApplier.applyImeVisibility(mWindowToken, statsToken, STATE_SHOW_IME,
-                    mUserId);
+                    eq(SoftInputShowHideReason.NOT_SET), mUserId);
         }
         verify(mMockWindowManagerInternal).showImePostLayout(eq(mWindowToken), eq(statsToken));
     }
@@ -126,7 +126,7 @@
         final var statsToken = ImeTracker.Token.empty();
         synchronized (ImfLock.class) {
             mVisibilityApplier.applyImeVisibility(mWindowToken, statsToken, STATE_HIDE_IME,
-                    mUserId);
+                    eq(SoftInputShowHideReason.NOT_SET), mUserId);
         }
         verify(mMockWindowManagerInternal).hideIme(eq(mWindowToken), anyInt() /* displayId */,
                 eq(statsToken));
@@ -137,7 +137,7 @@
         mInputMethodManagerService.mImeWindowVis = IME_ACTIVE;
         synchronized (ImfLock.class) {
             mVisibilityApplier.applyImeVisibility(mWindowToken, ImeTracker.Token.empty(),
-                    STATE_HIDE_IME_EXPLICIT, mUserId);
+                    STATE_HIDE_IME_EXPLICIT, eq(SoftInputShowHideReason.NOT_SET), mUserId);
         }
         verifyHideSoftInput(true, true);
     }
@@ -147,7 +147,7 @@
         mInputMethodManagerService.mImeWindowVis = IME_ACTIVE;
         synchronized (ImfLock.class) {
             mVisibilityApplier.applyImeVisibility(mWindowToken, ImeTracker.Token.empty(),
-                    STATE_HIDE_IME_NOT_ALWAYS, mUserId);
+                    STATE_HIDE_IME_NOT_ALWAYS, eq(SoftInputShowHideReason.NOT_SET), mUserId);
         }
         verifyHideSoftInput(true, true);
     }
@@ -156,7 +156,7 @@
     public void testApplyImeVisibility_showImeImplicit() throws Exception {
         synchronized (ImfLock.class) {
             mVisibilityApplier.applyImeVisibility(mWindowToken, ImeTracker.Token.empty(),
-                    STATE_SHOW_IME_IMPLICIT, mUserId);
+                    STATE_SHOW_IME_IMPLICIT, eq(SoftInputShowHideReason.NOT_SET), mUserId);
         }
         verifyShowSoftInput(true, true, 0 /* showFlags */);
     }
@@ -177,7 +177,7 @@
             // Verify hideIme will apply the expected displayId when the default IME
             // visibility applier app STATE_HIDE_IME.
             mVisibilityApplier.applyImeVisibility(mWindowToken, statsToken, STATE_HIDE_IME,
-                    mUserId);
+                    eq(SoftInputShowHideReason.NOT_SET), mUserId);
             verify(mInputMethodManagerService.mWindowManagerInternal).hideIme(
                     eq(mWindowToken), eq(displayIdToShowIme), eq(statsToken));
         }
@@ -217,14 +217,15 @@
             final int displayIdToShowIme = bindingController.getDisplayIdToShowIme();
             mInputMethodManagerService.hideCurrentInputLocked(mWindowToken,
                     statsToken, 0 /* flags */, null /* resultReceiver */,
-                    HIDE_SWITCH_USER);
-            mInputMethodManagerService.onUnbindCurrentMethodByReset();
+                    HIDE_SWITCH_USER, mUserId);
+            mInputMethodManagerService.onUnbindCurrentMethodByReset(mUserId);
 
             // Expects applyImeVisibility() -> hideIme() will be called to notify WM for syncing
             // the IME hidden state.
             // The unbind will cancel the previous stats token, and create a new one internally.
             verify(mVisibilityApplier).applyImeVisibility(
-                    eq(mWindowToken), any(), eq(STATE_HIDE_IME), eq(mUserId) /* userId */);
+                    eq(mWindowToken), any(), eq(STATE_HIDE_IME),
+                    eq(SoftInputShowHideReason.NOT_SET), eq(mUserId) /* userId */);
             verify(mInputMethodManagerService.mWindowManagerInternal).hideIme(
                     eq(mWindowToken), eq(displayIdToShowIme), and(not(eq(statsToken)), notNull()));
         }
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
index a22cacb..337d5c1 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
@@ -58,8 +58,8 @@
 import org.mockito.ArgumentCaptor;
 
 /**
- * Test the behavior of {@link ImeVisibilityStateComputer} and {@link ImeVisibilityApplier} when
- * requesting the IME visibility.
+ * Test the behavior of {@link ImeVisibilityStateComputer} and {@link DefaultImeVisibilityApplier}
+ * when requesting the IME visibility.
  *
  * <p> Build/Install/Run:
  * atest FrameworksInputMethodSystemServerTests:ImeVisibilityStateComputerTest
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
index 42bd75a..80eab11 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -189,6 +189,7 @@
         // Injecting and mocked InputMethodBindingController and InputMethod.
         mMockInputMethodInvoker = IInputMethodInvoker.create(mMockInputMethod);
         mInputManagerGlobalSession = InputManagerGlobal.createTestSession(mMockIInputManager);
+        when(mMockInputMethodBindingController.getUserId()).thenReturn(mCallingUserId);
         synchronized (ImfLock.class) {
             when(mMockInputMethodBindingController.getCurMethod())
                     .thenReturn(mMockInputMethodInvoker);
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java
index f9f45057..e2f3eec 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java
@@ -106,7 +106,7 @@
 
         // Assert UserDataRepository called the InputMethodBindingController creator function.
         verify(bindingControllerFactorySpy).apply(ANY_USER_ID);
-        assertThat(allUserData.get(0).mBindingController.mUserId).isEqualTo(ANY_USER_ID);
+        assertThat(allUserData.get(0).mBindingController.getUserId()).isEqualTo(ANY_USER_ID);
     }
 
     @Test
@@ -149,7 +149,7 @@
         assertThat(allUserData.get(0).mUserId).isEqualTo(ANY_USER_ID);
 
         // Assert UserDataRepository called the InputMethodBindingController creator function.
-        assertThat(allUserData.get(0).mBindingController.mUserId).isEqualTo(ANY_USER_ID);
+        assertThat(allUserData.get(0).mBindingController.getUserId()).isEqualTo(ANY_USER_ID);
     }
 
     private List<UserDataRepository.UserData> collectUserData(UserDataRepository repository) {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index bb774ee..7b8b712 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -1707,7 +1707,8 @@
         int initState = Display.STATE_OFF;
         mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
         mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
-        when(mDisplayOffloadSession.blockScreenOn(any())).thenReturn(true);
+        ArgumentCaptor<Runnable> argumentCaptor = ArgumentCaptor.forClass(Runnable.class);
+        when(mDisplayOffloadSession.blockScreenOn(argumentCaptor.capture())).thenReturn(true);
 
         // Start with OFF.
         when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
@@ -1721,8 +1722,7 @@
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        ArgumentCaptor<Runnable> argumentCaptor = ArgumentCaptor.forClass(Runnable.class);
-        verify(mDisplayOffloadSession).blockScreenOn(argumentCaptor.capture());
+        verify(mDisplayOffloadSession).blockScreenOn(any());
 
         // Unblocked
         argumentCaptor.getValue().run();
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsCollectorTest.java
index 0275319..ef20946 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsCollectorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsCollectorTest.java
@@ -39,6 +39,7 @@
 import android.os.BatteryStats;
 import android.os.Handler;
 import android.os.OutcomeReceiver;
+import android.os.connectivity.CellularBatteryStats;
 import android.platform.test.ravenwood.RavenwoodRule;
 import android.telephony.AccessNetworkConstants;
 import android.telephony.ActivityStatsTechSpecificInfo;
@@ -167,6 +168,7 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
         when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephony);
         when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)).thenReturn(true);
         when(mPowerStatsUidResolver.mapUid(anyInt())).thenAnswer(invocation -> {
             int uid = invocation.getArgument(0);
@@ -352,8 +354,48 @@
                 "UID 42: rx-pkts: 100 rx-B: 1000 tx-pkts: 200 tx-B: 2000");
     }
 
+    @Test
+    public void getCellularBatteryStats() throws Throwable {
+        mBatteryStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
+                true);
+
+        mockModemActivityInfo(1000, 2000, 3000, 600, new int[]{100, 200, 300, 400, 500});
+        mockNetworkStats(1100,
+                5321, 421, 3234, 223,
+                8000, 80, 4000, 40);
+
+        // This should trigger a baseline sample collection
+        mBatteryStats.onSystemReady(mContext);
+        mStatsRule.waitForBackgroundThread();
+
+        mockModemActivityInfo(20000, 2222, 3333, 666, new int[]{111, 222, 333, 444, 555});
+        mockNetworkStats(21000,
+                6321, 521, 7234, 423,
+                8888, 88, 4444, 44);
+
+        mStatsRule.setTime(30000, 30000);
+        mBatteryStats.getPowerStatsCollector(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO)
+                .schedule();
+        mStatsRule.waitForBackgroundThread();
+
+        CellularBatteryStats stats = mBatteryStats.getCellularBatteryStats();
+        assertThat(stats.getSleepTimeMillis()).isEqualTo(222);
+        assertThat(stats.getIdleTimeMillis()).isEqualTo(333);
+        assertThat(stats.getRxTimeMillis()).isEqualTo(66);
+        assertThat(stats.getTxTimeMillis(ModemActivityInfo.TX_POWER_LEVEL_0)).isEqualTo(11);
+        assertThat(stats.getTxTimeMillis(ModemActivityInfo.TX_POWER_LEVEL_1)).isEqualTo(22);
+        assertThat(stats.getTxTimeMillis(ModemActivityInfo.TX_POWER_LEVEL_2)).isEqualTo(33);
+        assertThat(stats.getTxTimeMillis(ModemActivityInfo.TX_POWER_LEVEL_3)).isEqualTo(44);
+        assertThat(stats.getTxTimeMillis(ModemActivityInfo.TX_POWER_LEVEL_4)).isEqualTo(55);
+        assertThat(stats.getNumPacketsRx()).isEqualTo(934);
+        assertThat(stats.getNumBytesRx()).isEqualTo(19967);
+        assertThat(stats.getNumPacketsTx()).isEqualTo(770);
+        assertThat(stats.getNumBytesTx()).isEqualTo(14214);
+    }
+
     private PowerStats collectPowerStats(boolean perNetworkTypeData) throws Throwable {
-        MobileRadioPowerStatsCollector collector = new MobileRadioPowerStatsCollector(mInjector);
+        MobileRadioPowerStatsCollector collector =
+                new MobileRadioPowerStatsCollector(mInjector, null);
         collector.setEnabled(true);
 
         when(mConsumedEnergyRetriever.getEnergyConsumerIds(
@@ -462,6 +504,7 @@
                     .addEntry(new NetworkStats.Entry("mobile", APP_UID3, 0, 0, METERED_NO,
                             ROAMING_NO, DEFAULT_NETWORK_NO, 314, 281, 314, 281, 111));
         }
+        mBatteryStats.setNetworkStats(stats);
         when(mNetworkStatsSupplier.get()).thenReturn(stats);
     }
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsProcessorTest.java
index 137c2a6..d7024e5 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsProcessorTest.java
@@ -191,7 +191,8 @@
         aggregatedStats.setUidState(APP_UID, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND, 0);
         aggregatedStats.setUidState(APP_UID2, STATE_PROCESS_STATE, PROCESS_STATE_CACHED, 0);
 
-        MobileRadioPowerStatsCollector collector = new MobileRadioPowerStatsCollector(mInjector);
+        MobileRadioPowerStatsCollector collector =
+                new MobileRadioPowerStatsCollector(mInjector, null);
         collector.setEnabled(true);
 
         // Initial empty ModemActivityInfo.
@@ -430,7 +431,8 @@
         aggregatedStats.setUidState(APP_UID, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND, 0);
         aggregatedStats.setUidState(APP_UID2, STATE_PROCESS_STATE, PROCESS_STATE_CACHED, 0);
 
-        MobileRadioPowerStatsCollector collector = new MobileRadioPowerStatsCollector(mInjector);
+        MobileRadioPowerStatsCollector collector =
+                new MobileRadioPowerStatsCollector(mInjector, null);
         collector.setEnabled(true);
 
         // Initial empty ModemActivityInfo.
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PhoneCallPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PhoneCallPowerStatsProcessorTest.java
index 548d54c..c268110 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/PhoneCallPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PhoneCallPowerStatsProcessorTest.java
@@ -180,7 +180,8 @@
         aggregatedPowerStats.setDeviceState(STATE_POWER, POWER_STATE_OTHER, 0);
         aggregatedPowerStats.setDeviceState(STATE_SCREEN, SCREEN_STATE_ON, 0);
 
-        MobileRadioPowerStatsCollector collector = new MobileRadioPowerStatsCollector(mInjector);
+        MobileRadioPowerStatsCollector collector =
+                new MobileRadioPowerStatsCollector(mInjector, null);
         collector.setEnabled(true);
 
         // Initial empty ModemActivityInfo.
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java
index a280cfe..362607b 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java
@@ -40,6 +40,7 @@
 import android.os.Handler;
 import android.os.WorkSource;
 import android.os.connectivity.WifiActivityEnergyInfo;
+import android.os.connectivity.WifiBatteryStats;
 import android.platform.test.ravenwood.RavenwoodRule;
 import android.util.IndentingPrintWriter;
 import android.util.SparseArray;
@@ -186,6 +187,7 @@
                 return uid;
             }
         });
+        when(mContext.getSystemService(Context.WIFI_SERVICE)).thenReturn(mWifiManager);
         mBatteryStats = mStatsRule.getBatteryStats();
     }
 
@@ -319,10 +321,51 @@
                         + " scan: 234 batched-scan: 345");
     }
 
+    @Test
+    public void getWifiBatteryStats() throws Throwable {
+        when(mWifiManager.isEnhancedPowerReportingSupported()).thenReturn(true);
+        mBatteryStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_WIFI,
+                true);
+
+        mockWifiActivityInfo(1000, 600, 100, 2000, 3000);
+        mockNetworkStats(1000);
+        mockNetworkStatsEntry(APP_UID1, 4321, 321, 1234, 23);
+        mockNetworkStatsEntry(APP_UID2, 4000, 40, 2000, 20);
+        mockWifiScanTimes(APP_UID1, 1000, 2000);
+        mockWifiScanTimes(APP_UID2, 3000, 4000);
+
+        // This should trigger a baseline sample collection
+        mBatteryStats.onSystemReady(mContext);
+        mStatsRule.waitForBackgroundThread();
+
+        mockWifiActivityInfo(1100, 6600, 1100, 2200, 3300);
+        mockNetworkStats(1100);
+        mockNetworkStatsEntry(APP_UID1, 5321, 421, 3234, 223);
+        mockNetworkStatsEntry(APP_UID2, 8000, 80, 4000, 40);
+        mockWifiScanTimes(APP_UID1, 1234, 2345);
+        mockWifiScanTimes(APP_UID2, 3100, 4200);
+
+        mStatsRule.setTime(30000, 30000);
+        mBatteryStats.getPowerStatsCollector(BatteryConsumer.POWER_COMPONENT_WIFI)
+                .schedule();
+        mStatsRule.waitForBackgroundThread();
+
+        WifiBatteryStats stats = mBatteryStats.getWifiBatteryStats();
+        assertThat(stats.getNumPacketsRx()).isEqualTo(501);
+        assertThat(stats.getNumBytesRx()).isEqualTo(13321);
+        assertThat(stats.getNumPacketsTx()).isEqualTo(263);
+        assertThat(stats.getNumBytesTx()).isEqualTo(7234);
+        assertThat(stats.getScanTimeMillis()).isEqualTo(2200);
+        assertThat(stats.getRxTimeMillis()).isEqualTo(6000);
+        assertThat(stats.getTxTimeMillis()).isEqualTo(1000);
+        assertThat(stats.getIdleTimeMillis()).isEqualTo(300);
+        assertThat(stats.getSleepTimeMillis()).isEqualTo(30000 - 6000 - 1000 - 300);
+    }
+
     private PowerStats collectPowerStats(boolean hasPowerReporting) {
         when(mWifiManager.isEnhancedPowerReportingSupported()).thenReturn(hasPowerReporting);
 
-        WifiPowerStatsCollector collector = new WifiPowerStatsCollector(mInjector);
+        WifiPowerStatsCollector collector = new WifiPowerStatsCollector(mInjector, null);
         collector.setEnabled(true);
 
         when(mConsumedEnergyRetriever.getEnergyConsumerIds(EnergyConsumerType.WIFI))
@@ -389,6 +432,7 @@
         } else {
             mNetworkStats = new NetworkStats(elapsedRealtime, 1);
         }
+        mBatteryStats.setNetworkStats(mNetworkStats);
         when(mNetworkStatsSupplier.get()).thenReturn(mNetworkStats);
     }
 
@@ -411,6 +455,7 @@
                     .addEntry(new NetworkStats.Entry("wifi", uid, 0, 0,
                             METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, rxBytes, rxPackets,
                             txBytes, txPackets, 100));
+            mBatteryStats.setNetworkStats(mNetworkStats);
             reset(mNetworkStatsSupplier);
             when(mNetworkStatsSupplier.get()).thenReturn(mNetworkStats);
         }
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsProcessorTest.java
index ff56691..7ddaefd 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsProcessorTest.java
@@ -99,6 +99,8 @@
     @Mock
     private WifiManager mWifiManager;
 
+    private MockBatteryStatsImpl mBatteryStats;
+
     private static class ScanTimes {
         public long scanTimeMs;
         public long batchScanTimeMs;
@@ -185,6 +187,8 @@
         when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
         when(mPowerStatsUidResolver.mapUid(anyInt()))
                 .thenAnswer(invocation -> invocation.getArgument(0));
+
+        mBatteryStats = mStatsRule.getBatteryStats();
     }
 
     @Test
@@ -200,7 +204,7 @@
 
         PowerComponentAggregatedPowerStats aggregatedStats = createAggregatedPowerStats(processor);
 
-        WifiPowerStatsCollector collector = new WifiPowerStatsCollector(mInjector);
+        WifiPowerStatsCollector collector = new WifiPowerStatsCollector(mInjector, null);
         collector.setEnabled(true);
 
         // Initial empty WifiActivityEnergyInfo.
@@ -312,7 +316,7 @@
 
         PowerComponentAggregatedPowerStats aggregatedStats = createAggregatedPowerStats(processor);
 
-        WifiPowerStatsCollector collector = new WifiPowerStatsCollector(mInjector);
+        WifiPowerStatsCollector collector = new WifiPowerStatsCollector(mInjector, null);
         collector.setEnabled(true);
 
         // Initial empty WifiActivityEnergyInfo.
@@ -425,7 +429,7 @@
 
         PowerComponentAggregatedPowerStats aggregatedStats = createAggregatedPowerStats(processor);
 
-        WifiPowerStatsCollector collector = new WifiPowerStatsCollector(mInjector);
+        WifiPowerStatsCollector collector = new WifiPowerStatsCollector(mInjector, null);
         collector.setEnabled(true);
 
         // Establish a baseline
diff --git a/services/tests/servicestests/src/com/android/server/autofill/RequestIdTest.java b/services/tests/servicestests/src/com/android/server/autofill/RequestIdTest.java
index 6d56c41..60c3659 100644
--- a/services/tests/servicestests/src/com/android/server/autofill/RequestIdTest.java
+++ b/services/tests/servicestests/src/com/android/server/autofill/RequestIdTest.java
@@ -17,17 +17,25 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-import java.util.ArrayList;
-import java.util.List;
-
 @RunWith(JUnit4.class)
 public class RequestIdTest {
 
+    private static final int TEST_DATASET_SIZE = 300;
+    private static final int TEST_WRAP_SIZE = 50; // Number of request ids before wrap happens
+    private static final String TAG = "RequestIdTest";
+
     List<Integer> datasetPrimaryNoWrap = new ArrayList<>();
     List<Integer> datasetPrimaryWrap = new ArrayList<>();
     List<Integer> datasetSecondaryNoWrap = new ArrayList<>();
@@ -35,151 +43,200 @@
     List<Integer> datasetMixedNoWrap = new ArrayList<>();
     List<Integer> datasetMixedWrap = new ArrayList<>();
 
-    @Before
-    public void setup() throws Exception {
-      int datasetSize = 300;
+    List<Integer> manualWrapRequestIdList = Arrays.asList(3, 9, 15,
+                                                            RequestId.MAX_SECONDARY_REQUEST_ID - 5,
+                                                            RequestId.MAX_SECONDARY_REQUEST_ID - 3);
+    List<Integer> manualNoWrapRequestIdList =Arrays.asList(2, 6, 10, 14, 18, 22, 26, 30);
 
+    List<Integer> manualOneElementRequestIdList = Arrays.asList(1);
+
+    @Before
+    public void setup() throws IllegalArgumentException {
+        Slog.d(TAG, "setup()");
         { // Generate primary only ids that do not wrap
-            RequestId requestId = new RequestId(0);
-            for (int i = 0; i < datasetSize; i++) {
+            RequestId requestId = new RequestId(RequestId.MIN_PRIMARY_REQUEST_ID);
+            for (int i = 0; i < TEST_DATASET_SIZE; i++) {
                 datasetPrimaryNoWrap.add(requestId.nextId(false));
             }
+            Collections.sort(datasetPrimaryNoWrap);
         }
 
         { // Generate primary only ids that wrap
-            RequestId requestId = new RequestId(0xff00);
-            for (int i = 0; i < datasetSize; i++) {
+            RequestId requestId = new RequestId(RequestId.MAX_PRIMARY_REQUEST_ID -
+                                                    TEST_WRAP_SIZE * 2);
+            for (int i = 0; i < TEST_DATASET_SIZE; i++) {
                 datasetPrimaryWrap.add(requestId.nextId(false));
             }
+            Collections.sort(datasetPrimaryWrap);
         }
 
         { // Generate SECONDARY only ids that do not wrap
-            RequestId requestId = new RequestId(0);
-            for (int i = 0; i < datasetSize; i++) {
+            RequestId requestId = new RequestId(RequestId.MIN_SECONDARY_REQUEST_ID);
+            for (int i = 0; i < TEST_DATASET_SIZE; i++) {
                 datasetSecondaryNoWrap.add(requestId.nextId(true));
             }
+            Collections.sort(datasetSecondaryNoWrap);
         }
 
         { // Generate SECONDARY only ids that wrap
-            RequestId requestId = new RequestId(0xff00);
-            for (int i = 0; i < datasetSize; i++) {
+            RequestId requestId = new RequestId(RequestId.MAX_SECONDARY_REQUEST_ID -
+                                                    TEST_WRAP_SIZE * 2);
+            for (int i = 0; i < TEST_DATASET_SIZE; i++) {
                 datasetSecondaryWrap.add(requestId.nextId(true));
             }
+            Collections.sort(datasetSecondaryWrap);
         }
 
         { // Generate MIXED only ids that do not wrap
-            RequestId requestId = new RequestId(0);
-            for (int i = 0; i < datasetSize; i++) {
+            RequestId requestId = new RequestId(RequestId.MIN_REQUEST_ID);
+            for (int i = 0; i < TEST_DATASET_SIZE; i++) {
                 datasetMixedNoWrap.add(requestId.nextId(i % 2 != 0));
             }
+            Collections.sort(datasetMixedNoWrap);
         }
 
         { // Generate MIXED only ids that wrap
-            RequestId requestId = new RequestId(0xff00);
-            for (int i = 0; i < datasetSize; i++) {
+            RequestId requestId = new RequestId(RequestId.MAX_REQUEST_ID -
+                                                    TEST_WRAP_SIZE);
+            for (int i = 0; i < TEST_DATASET_SIZE; i++) {
                 datasetMixedWrap.add(requestId.nextId(i % 2 != 0));
             }
+            Collections.sort(datasetMixedWrap);
         }
+        Slog.d(TAG, "finishing setup()");
     }
 
     @Test
     public void testRequestIdLists() {
+        Slog.d(TAG, "testRequestIdLists()");
         for (int id : datasetPrimaryNoWrap) {
             assertThat(RequestId.isSecondaryProvider(id)).isFalse();
-            assertThat(id >= 0).isTrue();
-            assertThat(id < 0xffff).isTrue();
+            assertThat(id).isAtLeast(RequestId.MIN_PRIMARY_REQUEST_ID);
+            assertThat(id).isAtMost(RequestId.MAX_PRIMARY_REQUEST_ID);
         }
 
         for (int id : datasetPrimaryWrap) {
             assertThat(RequestId.isSecondaryProvider(id)).isFalse();
-            assertThat(id >= 0).isTrue();
-            assertThat(id < 0xffff).isTrue();
+            assertThat(id).isAtLeast(RequestId.MIN_PRIMARY_REQUEST_ID);
+            assertThat(id).isAtMost(RequestId.MAX_PRIMARY_REQUEST_ID);
         }
 
         for (int id : datasetSecondaryNoWrap) {
             assertThat(RequestId.isSecondaryProvider(id)).isTrue();
-            assertThat(id >= 0).isTrue();
-            assertThat(id < 0xffff).isTrue();
+            assertThat(id).isAtLeast(RequestId.MIN_SECONDARY_REQUEST_ID);
+            assertThat(id).isAtMost(RequestId.MAX_SECONDARY_REQUEST_ID);
         }
 
         for (int id : datasetSecondaryWrap) {
             assertThat(RequestId.isSecondaryProvider(id)).isTrue();
-            assertThat(id >= 0).isTrue();
-            assertThat(id < 0xffff).isTrue();
+            assertThat(id).isAtLeast(RequestId.MIN_SECONDARY_REQUEST_ID);
+            assertThat(id).isAtMost(RequestId.MAX_SECONDARY_REQUEST_ID);
         }
     }
 
     @Test
-    public void testRequestIdGeneration() {
-        RequestId requestId = new RequestId(0);
+    public void testCreateNewRequestId() {
+        Slog.d(TAG, "testCreateNewRequestId()");
+        for (int i = 0; i < 100000; i++) {
+            RequestId requestId = new RequestId();
+            assertThat(requestId.getRequestId()).isAtLeast(RequestId.MIN_REQUEST_ID);
+            assertThat(requestId.getRequestId()).isAtMost(RequestId.MAX_START_ID);
+        }
+    }
 
+    @Test
+    public void testGetNextRequestId() throws IllegalArgumentException{
+        Slog.d(TAG, "testGetNextRequestId()");
+        RequestId requestId = new RequestId();
         // Large Primary
         for (int i = 0; i < 100000; i++) {
             int y = requestId.nextId(false);
             assertThat(RequestId.isSecondaryProvider(y)).isFalse();
-            assertThat(y >= 0).isTrue();
-            assertThat(y < 0xffff).isTrue();
+            assertThat(y).isAtLeast(RequestId.MIN_PRIMARY_REQUEST_ID);
+            assertThat(y).isAtMost(RequestId.MAX_PRIMARY_REQUEST_ID);
         }
 
         // Large Secondary
-        requestId = new RequestId(0);
+        requestId = new RequestId();
         for (int i = 0; i < 100000; i++) {
             int y = requestId.nextId(true);
             assertThat(RequestId.isSecondaryProvider(y)).isTrue();
-            assertThat(y >= 0).isTrue();
-            assertThat(y < 0xffff).isTrue();
+            assertThat(y).isAtLeast(RequestId.MIN_SECONDARY_REQUEST_ID);
+            assertThat(y).isAtMost(RequestId.MAX_SECONDARY_REQUEST_ID);
         }
 
         // Large Mixed
-        requestId = new RequestId(0);
+        requestId = new RequestId();
         for (int i = 0; i < 50000; i++) {
             int y = requestId.nextId(i % 2 != 0);
-            assertThat(RequestId.isSecondaryProvider(y)).isEqualTo(i % 2 == 0);
-            assertThat(y >= 0).isTrue();
-            assertThat(y < 0xffff).isTrue();
+            assertThat(y).isAtLeast(RequestId.MIN_REQUEST_ID);
+            assertThat(y).isAtMost(RequestId.MAX_REQUEST_ID);
         }
     }
 
     @Test
     public void testGetLastRequestId() {
-        // In this test, request ids are generated FIFO, so the last entry is also the last
-        // request
+        Slog.d(TAG, "testGetLastRequestId()");
 
-        { // Primary no wrap
-          int lastIdIndex = datasetPrimaryNoWrap.size() - 1;
-          int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetPrimaryNoWrap);
-          assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex);
-        }
-
-        { // Primary wrap
-            int lastIdIndex = datasetPrimaryWrap.size() - 1;
-            int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetPrimaryWrap);
+        {   // Primary no wrap
+            int lastIdIndex = datasetPrimaryNoWrap.size() - 1;
+            int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetPrimaryNoWrap);
             assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex);
         }
 
-        { // Secondary no wrap
+        {   // Primary wrap
+            // The last index would be the # of request ids left after wrap
+            // minus 1 (index starts at 0)
+            int lastIdIndex = TEST_DATASET_SIZE - TEST_WRAP_SIZE - 1;
+            int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetPrimaryWrap);
+            assertThat(lastComputedIdIndex).isEqualTo(lastIdIndex);
+        }
+
+        {   // Secondary no wrap
             int lastIdIndex = datasetSecondaryNoWrap.size() - 1;
             int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetSecondaryNoWrap);
             assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex);
         }
 
-        { // Secondary wrap
-            int lastIdIndex = datasetSecondaryWrap.size() - 1;
+        {   // Secondary wrap
+            int lastIdIndex = TEST_DATASET_SIZE - TEST_WRAP_SIZE - 1;
             int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetSecondaryWrap);
             assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex);
         }
 
-        { // Mixed no wrap
+        {   // Mixed no wrap
             int lastIdIndex = datasetMixedNoWrap.size() - 1;
             int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetMixedNoWrap);
             assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex);
         }
 
-        { // Mixed wrap
-            int lastIdIndex = datasetMixedWrap.size() - 1;
+        {   // Mixed wrap
+            int lastIdIndex = TEST_DATASET_SIZE - TEST_WRAP_SIZE - 1;
             int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetMixedWrap);
             assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex);
         }
 
+        {   // Manual wrap
+            int lastIdIndex = 2; // [3, 9, 15,
+                                 // MAX_SECONDARY_REQUEST_ID - 5, MAX_SECONDARY_REQUEST_ID - 3]
+            int lastComputedIdIndex = RequestId.getLastRequestIdIndex(manualWrapRequestIdList);
+            assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex);
+        }
+
+        {   // Manual no wrap
+            int lastIdIndex = manualNoWrapRequestIdList.size() - 1; // [2, 6, 10, 14,
+                                                                    // 18, 22, 26, 30]
+            int lastComputedIdIndex = RequestId.getLastRequestIdIndex(manualNoWrapRequestIdList);
+            assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex);
+
+        }
+
+        {   // Manual one element
+            int lastIdIndex = 0; // [1]
+            int lastComputedIdIndex = RequestId.getLastRequestIdIndex(
+                manualOneElementRequestIdList);
+            assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex);
+
+        }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java
index 9cd3186..36a5cda 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java
@@ -393,6 +393,18 @@
         testAuthenticate_throwsSecurityException(promptInfo);
     }
 
+    @Test
+    public void testCanAuthenticate_throwsWhenUsingAdvancedApis() {
+        mAuthService = new AuthService(mContext, mInjector);
+        mAuthService.onStart();
+
+        assertThrows(SecurityException.class, () -> {
+            mAuthService.mImpl.canAuthenticate(TEST_OP_PACKAGE_NAME, 1 /* userId */,
+                    BiometricManager.Authenticators.MANDATORY_BIOMETRICS);
+            waitForIdle();
+        });
+    }
+
     private void testAuthenticate_throwsSecurityException(PromptInfo promptInfo) {
         mAuthService = new AuthService(mContext, mInjector);
         mAuthService.onStart();
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java
index 0678140..e078238 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java
@@ -92,8 +92,6 @@
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     @Mock
-    private GenericWindowPolicyController.PipBlockedCallback mPipBlockedCallback;
-    @Mock
     private VirtualDeviceManager.ActivityListener mActivityListener;
     @Mock
     private GenericWindowPolicyController.IntentListenerCallback mIntentListenerCallback;
@@ -140,7 +138,6 @@
         gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
 
         assertThat(gwpc.isEnteringPipAllowed(TEST_UID)).isFalse();
-        verify(mPipBlockedCallback, timeout(TIMEOUT_MILLIS)).onEnteringPipBlocked(TEST_UID);
     }
 
     @Test
@@ -151,7 +148,6 @@
                 Arrays.asList(WindowConfiguration.WINDOWING_MODE_FULLSCREEN,
                         WindowConfiguration.WINDOWING_MODE_PINNED)));
         assertThat(gwpc.isEnteringPipAllowed(TEST_UID)).isTrue();
-        verify(mPipBlockedCallback, after(TIMEOUT_MILLIS).never()).onEnteringPipBlocked(TEST_UID);
     }
 
     @Test
@@ -746,7 +742,6 @@
                 /* crossTaskNavigationExemptions= */ new ArraySet<>(),
                 /* permissionDialogComponent= */ null,
                 /* activityListener= */ mActivityListener,
-                /* pipBlockedCallback= */ mPipBlockedCallback,
                 /* activityBlockedCallback= */ mActivityBlockedCallback,
                 /* secureWindowCallback= */ mSecureWindowCallback,
                 /* intentListenerCallback= */ mIntentListenerCallback,
@@ -767,7 +762,6 @@
                 /* crossTaskNavigationExemptions= */ new ArraySet<>(),
                 /* permissionDialogComponent= */ null,
                 /* activityListener= */ mActivityListener,
-                /* pipBlockedCallback= */ mPipBlockedCallback,
                 /* activityBlockedCallback= */ mActivityBlockedCallback,
                 /* secureWindowCallback= */ mSecureWindowCallback,
                 /* intentListenerCallback= */ mIntentListenerCallback,
@@ -789,7 +783,6 @@
                 /* crossTaskNavigationExemptions= */ new ArraySet<>(),
                 /* permissionDialogComponent= */ null,
                 /* activityListener= */ mActivityListener,
-                /* pipBlockedCallback= */ mPipBlockedCallback,
                 /* activityBlockedCallback= */ mActivityBlockedCallback,
                 /* secureWindowCallback= */ null,
                 /* intentListenerCallback= */ mIntentListenerCallback,
@@ -811,7 +804,6 @@
                 /* crossTaskNavigationExemptions= */ new ArraySet<>(),
                 /* permissionDialogComponent= */ null,
                 /* activityListener= */ mActivityListener,
-                /* pipBlockedCallback= */ mPipBlockedCallback,
                 /* activityBlockedCallback= */ mActivityBlockedCallback,
                 /* secureWindowCallback= */ null,
                 /* intentListenerCallback= */ mIntentListenerCallback,
@@ -833,7 +825,6 @@
                 /* crossTaskNavigationExemptions= */ new ArraySet<>(),
                 /* permissionDialogComponent= */ null,
                 /* activityListener= */ mActivityListener,
-                /* pipBlockedCallback= */ mPipBlockedCallback,
                 /* activityBlockedCallback= */ mActivityBlockedCallback,
                 /* secureWindowCallback= */ null,
                 /* intentListenerCallback= */ mIntentListenerCallback,
@@ -855,7 +846,6 @@
                 /* crossTaskNavigationExemptions= */ new ArraySet<>(),
                 /* permissionDialogComponent= */ null,
                 /* activityListener= */ mActivityListener,
-                /* pipBlockedCallback= */ mPipBlockedCallback,
                 /* activityBlockedCallback= */ mActivityBlockedCallback,
                 /* secureWindowCallback= */ null,
                 /* intentListenerCallback= */ mIntentListenerCallback,
@@ -877,7 +867,6 @@
                 /* crossTaskNavigationExemptions= */ Collections.singleton(blockedComponent),
                 /* permissionDialogComponent= */ null,
                 /* activityListener= */ mActivityListener,
-                /* pipBlockedCallback= */ mPipBlockedCallback,
                 /* activityBlockedCallback= */ mActivityBlockedCallback,
                 /* secureWindowCallback= */ null,
                 /* intentListenerCallback= */ mIntentListenerCallback,
@@ -899,7 +888,6 @@
                 /* crossTaskNavigationExemptions= */ Collections.singleton(allowedComponent),
                 /* permissionDialogComponent= */ null,
                 /* activityListener= */ mActivityListener,
-                /* pipBlockedCallback= */ mPipBlockedCallback,
                 /* activityBlockedCallback= */ mActivityBlockedCallback,
                 /* secureWindowCallback= */ null,
                 /* intentListenerCallback= */ mIntentListenerCallback,
@@ -922,7 +910,6 @@
                 /* crossTaskNavigationExemptions= */ new ArraySet<>(),
                 /* permissionDialogComponent= */ permissionComponent,
                 /* activityListener= */ mActivityListener,
-                /* pipBlockedCallback= */ mPipBlockedCallback,
                 /* activityBlockedCallback= */ mActivityBlockedCallback,
                 /* secureWindowCallback= */ null,
                 /* intentListenerCallback= */ mIntentListenerCallback,
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
index 52f28b9..b946a43 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
@@ -87,7 +87,6 @@
                         /* crossTaskNavigationExemptions= */ new ArraySet<>(),
                         /* permissionDialogComponent */ null,
                         /* activityListener= */ null,
-                        /* pipBlockedCallback= */ null,
                         /* activityBlockedCallback= */ null,
                         /* secureWindowCallback= */ null,
                         /* intentListenerCallback= */ null,
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
index 7d9fdd5..3fcf304 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
@@ -24,9 +24,12 @@
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 import static org.testng.Assert.assertFalse;
 
 import android.annotation.Nullable;
@@ -55,6 +58,7 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
+import java.util.function.Consumer;
 
 /**
  * Tests for the {@link DisplayWindowSettingsProvider} class.
@@ -128,9 +132,8 @@
         // Update settings with new value, should trigger write to injector.
         DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider(
                 mDefaultVendorSettingsStorage, mOverrideSettingsStorage);
-        SettingsEntry overrideSettings = provider.getOverrideSettings(mPrimaryDisplayInfo);
-        overrideSettings.mForcedDensity = 200;
-        provider.updateOverrideSettings(mPrimaryDisplayInfo, overrideSettings);
+        updateOverrideSettings(provider, mPrimaryDisplayInfo,
+                overrideSettings -> overrideSettings.mForcedDensity = 200);
         assertTrue(mOverrideSettingsStorage.wasWriteSuccessful());
 
         // Verify that display identifier was updated.
@@ -167,7 +170,7 @@
     }
 
     @Test
-    public void testReadingDisplaySettingsFromStorage_secondayVendorDisplaySettingsLocation() {
+    public void testReadingDisplaySettingsFromStorage_secondaryVendorDisplaySettingsLocation() {
         final String displayIdentifier = mSecondaryDisplay.getDisplayInfo().uniqueId;
         prepareSecondaryDisplaySettings(displayIdentifier);
 
@@ -216,11 +219,11 @@
         // Write some settings to storage.
         DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider(
                 mDefaultVendorSettingsStorage, mOverrideSettingsStorage);
-        SettingsEntry overrideSettings = provider.getOverrideSettings(secondaryDisplayInfo);
-        overrideSettings.mShouldShowSystemDecors = true;
-        overrideSettings.mImePolicy = DISPLAY_IME_POLICY_LOCAL;
-        overrideSettings.mDontMoveToTop = true;
-        provider.updateOverrideSettings(secondaryDisplayInfo, overrideSettings);
+        updateOverrideSettings(provider, secondaryDisplayInfo, overrideSettings -> {
+            overrideSettings.mShouldShowSystemDecors = true;
+            overrideSettings.mImePolicy = DISPLAY_IME_POLICY_LOCAL;
+            overrideSettings.mDontMoveToTop = true;
+        });
         assertTrue(mOverrideSettingsStorage.wasWriteSuccessful());
 
         // Verify that settings were stored correctly.
@@ -235,6 +238,29 @@
     }
 
     @Test
+    public void testWritingDisplaySettingsToStorage_secondaryUserDisplaySettingsLocation() {
+        final DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider(
+                mDefaultVendorSettingsStorage, mOverrideSettingsStorage);
+        final DisplayInfo displayInfo = mPrimaryDisplay.getDisplayInfo();
+        final TestStorage secondaryUserOverrideSettingsStorage = new TestStorage();
+        final SettingsEntry expectedSettings = new SettingsEntry();
+        expectedSettings.mForcedDensity = 356;
+
+        // Write some settings to storage from default user.
+        updateOverrideSettings(provider, displayInfo, settings -> settings.mForcedDensity = 356);
+        assertThat(mOverrideSettingsStorage.wasWriteSuccessful()).isTrue();
+
+        // Now switch to secondary user override settings and write some settings.
+        provider.setOverrideSettingsStorage(secondaryUserOverrideSettingsStorage);
+        updateOverrideSettings(provider, displayInfo, settings -> settings.mForcedDensity = 420);
+        assertThat(secondaryUserOverrideSettingsStorage.wasWriteSuccessful()).isTrue();
+
+        // Switch back to primary and assert default user settings remain unchanged.
+        provider.setOverrideSettingsStorage(mOverrideSettingsStorage);
+        assertThat(provider.getOverrideSettings(displayInfo)).isEqualTo(expectedSettings);
+    }
+
+    @Test
     public void testDoNotWriteVirtualDisplaySettingsToStorage() throws Exception {
         final DisplayInfo secondaryDisplayInfo = mSecondaryDisplay.getDisplayInfo();
         secondaryDisplayInfo.type = TYPE_VIRTUAL;
@@ -242,11 +268,11 @@
         // No write to storage on virtual display change.
         final DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider(
                 mDefaultVendorSettingsStorage, mOverrideSettingsStorage);
-        final SettingsEntry virtualSettings = provider.getOverrideSettings(secondaryDisplayInfo);
-        virtualSettings.mShouldShowSystemDecors = true;
-        virtualSettings.mImePolicy = DISPLAY_IME_POLICY_LOCAL;
-        virtualSettings.mDontMoveToTop = true;
-        provider.updateOverrideSettings(secondaryDisplayInfo, virtualSettings);
+        updateOverrideSettings(provider, secondaryDisplayInfo, virtualSettings -> {
+            virtualSettings.mShouldShowSystemDecors = true;
+            virtualSettings.mImePolicy = DISPLAY_IME_POLICY_LOCAL;
+            virtualSettings.mDontMoveToTop = true;
+        });
         assertFalse(mOverrideSettingsStorage.wasWriteSuccessful());
     }
 
@@ -263,10 +289,10 @@
         // Write some settings to storage.
         DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider(
                 mDefaultVendorSettingsStorage, mOverrideSettingsStorage);
-        SettingsEntry overrideSettings = provider.getOverrideSettings(secondaryDisplayInfo);
-        overrideSettings.mShouldShowSystemDecors = true;
-        overrideSettings.mImePolicy = DISPLAY_IME_POLICY_LOCAL;
-        provider.updateOverrideSettings(secondaryDisplayInfo, overrideSettings);
+        updateOverrideSettings(provider, secondaryDisplayInfo, overrideSettings -> {
+            overrideSettings.mShouldShowSystemDecors = true;
+            overrideSettings.mImePolicy = DISPLAY_IME_POLICY_LOCAL;
+        });
         assertTrue(mOverrideSettingsStorage.wasWriteSuccessful());
 
         // Verify that settings were stored correctly.
@@ -283,16 +309,16 @@
         final DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider(
                 mDefaultVendorSettingsStorage, mOverrideSettingsStorage);
         final int initialSize = provider.getOverrideSettingsSize();
-
-        // Size + 1 when query for a new display.
         final DisplayInfo secondaryDisplayInfo = mSecondaryDisplay.getDisplayInfo();
-        final SettingsEntry overrideSettings = provider.getOverrideSettings(secondaryDisplayInfo);
 
-        assertEquals(initialSize + 1, provider.getOverrideSettingsSize());
+        updateOverrideSettings(provider, secondaryDisplayInfo, overrideSettings -> {
+            // Size + 1 when query for a new display.
+            assertEquals(initialSize + 1, provider.getOverrideSettingsSize());
 
-        // When a display is removed, its override Settings is not removed if there is any override.
-        overrideSettings.mShouldShowSystemDecors = true;
-        provider.updateOverrideSettings(secondaryDisplayInfo, overrideSettings);
+            // When a display is removed, its override Settings is not removed if there is any
+            // override.
+            overrideSettings.mShouldShowSystemDecors = true;
+        });
         provider.onDisplayRemoved(secondaryDisplayInfo);
 
         assertEquals(initialSize + 1, provider.getOverrideSettingsSize());
@@ -309,23 +335,53 @@
         final DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider(
                 mDefaultVendorSettingsStorage, mOverrideSettingsStorage);
         final int initialSize = provider.getOverrideSettingsSize();
-
-        // Size + 1 when query for a new display.
         final DisplayInfo secondaryDisplayInfo = mSecondaryDisplay.getDisplayInfo();
         secondaryDisplayInfo.type = TYPE_VIRTUAL;
-        final SettingsEntry overrideSettings = provider.getOverrideSettings(secondaryDisplayInfo);
 
-        assertEquals(initialSize + 1, provider.getOverrideSettingsSize());
+        updateOverrideSettings(provider, secondaryDisplayInfo, overrideSettings -> {
+            // Size + 1 when query for a new display.
+            assertEquals(initialSize + 1, provider.getOverrideSettingsSize());
 
-        // When a virtual display is removed, its override Settings is removed even if it has
-        // override.
-        overrideSettings.mShouldShowSystemDecors = true;
-        provider.updateOverrideSettings(secondaryDisplayInfo, overrideSettings);
+            // When a virtual display is removed, its override Settings is removed
+            // even if it has override.
+            overrideSettings.mShouldShowSystemDecors = true;
+        });
         provider.onDisplayRemoved(secondaryDisplayInfo);
 
         assertEquals(initialSize, provider.getOverrideSettingsSize());
     }
 
+    @Test
+    public void testRemovesStaleDisplaySettings() {
+        assumeTrue(com.android.window.flags.Flags.perUserDisplayWindowSettings());
+
+        final DisplayWindowSettingsProvider provider =
+                new DisplayWindowSettingsProvider(mDefaultVendorSettingsStorage,
+                        mOverrideSettingsStorage);
+        final DisplayInfo displayInfo = mSecondaryDisplay.getDisplayInfo();
+        updateOverrideSettings(provider, displayInfo, settings -> settings.mForcedDensity = 356);
+        mRootWindowContainer.removeChild(mSecondaryDisplay);
+
+        provider.removeStaleDisplaySettings(mRootWindowContainer);
+
+        assertThat(mOverrideSettingsStorage.wasWriteSuccessful()).isTrue();
+        assertThat(provider.getOverrideSettingsSize()).isEqualTo(0);
+    }
+
+    /**
+     * Updates the override settings for a specific display.
+     *
+     * @param provider the provider to obtain and update the settings from.
+     * @param displayInfo the information about the display to be updated.
+     * @param modifier a function that modifies the settings for the display.
+     */
+    private static void updateOverrideSettings(DisplayWindowSettingsProvider provider,
+            DisplayInfo displayInfo, Consumer<SettingsEntry> modifier) {
+        final SettingsEntry settings = provider.getOverrideSettings(displayInfo);
+        modifier.accept(settings);
+        provider.updateOverrideSettings(displayInfo, settings);
+    }
+
     /**
      * Prepares display settings and stores in {@link #mOverrideSettingsStorage}. Uses provided
      * display identifier and stores windowingMode=WINDOWING_MODE_PINNED.
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
index eb79118..3078df0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -392,6 +392,8 @@
         assertEquals(newPipTask, mDisplayContent.getDefaultTaskDisplayArea().getRootPinnedTask());
         assertNotEquals(newPipTask, activity1.getTask());
         assertFalse("Created PiP task must not be in recents", newPipTask.inRecents);
+        assertThat(newPipTask.autoRemoveRecents).isTrue();
+        assertThat(activity1.getTask().autoRemoveRecents).isFalse();
     }
 
     /**
@@ -427,6 +429,7 @@
         bounds.scale(0.5f);
         task.setBounds(bounds);
         assertFalse(activity.isLetterboxedForFixedOrientationAndAspectRatio());
+        assertThat(task.autoRemoveRecents).isFalse();
     }
 
     /**
@@ -451,6 +454,7 @@
         // Ensure a task has moved over.
         ensureTaskPlacement(task, activity);
         assertTrue(task.inPinnedWindowingMode());
+        assertThat(task.autoRemoveRecents).isFalse();
     }
 
     /**
@@ -480,6 +484,8 @@
         ensureTaskPlacement(fullscreenTask, secondActivity);
         assertTrue(pinnedRootTask.inPinnedWindowingMode());
         assertEquals(WINDOWING_MODE_FULLSCREEN, fullscreenTask.getWindowingMode());
+        assertThat(pinnedRootTask.autoRemoveRecents).isTrue();
+        assertThat(secondActivity.getTask().autoRemoveRecents).isFalse();
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index e01cea3..ef0aa9e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -42,6 +42,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.server.policy.WindowManagerPolicy.USER_ROTATION_FREE;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
 import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG;
 import static com.android.server.wm.TaskFragment.EMBEDDED_DIM_AREA_PARENT_TASK;
 import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_VISIBLE_BEHIND_TRANSLUCENT;
@@ -222,6 +223,27 @@
     }
 
     @Test
+    public void testReparentPinnedActivityBackToOriginalTask() {
+        final ActivityRecord activityMain = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final Task originalTask = activityMain.getTask();
+        final ActivityRecord activityPip = new ActivityBuilder(mAtm).setTask(originalTask).build();
+        activityPip.setState(RESUMED, "test");
+        mAtm.mRootWindowContainer.moveActivityToPinnedRootTask(activityPip,
+                null /* launchIntoPipHostActivity */, "test");
+        final Task pinnedActivityTask = activityPip.getTask();
+
+        // Simulate pinnedActivityTask unintentionally added to recent during top activity resume.
+        mAtm.getRecentTasks().getRawTasks().add(pinnedActivityTask);
+
+        // Reparent the activity back to its original task when exiting PIP mode.
+        pinnedActivityTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+
+        assertThat(activityPip.getTask()).isEqualTo(originalTask);
+        assertThat(originalTask.autoRemoveRecents).isFalse();
+        assertThat(mAtm.getRecentTasks().getRawTasks()).containsExactly(originalTask);
+    }
+
+    @Test
     public void testReparent_BetweenDisplays() {
         // Create first task on primary display.
         final Task rootTask1 = createTask(mDisplayContent);
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 27c383c..bf46154 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -113,6 +113,7 @@
 import com.android.server.IoThread;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
+import com.android.server.pm.UserManagerInternal;
 import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener;
 import com.android.server.utils.AlarmQueue;
 
@@ -1063,6 +1064,18 @@
         synchronized (mReportedEvents) {
             LinkedList<Event> events = mReportedEvents.get(userId);
             if (events == null) {
+                // TODO (b/347644400): callers of this API should verify that the userId passed to
+                // this method exists - there is currently a known case where USER_ALL is passed
+                // here and it would be added to the queue, never to be flushed correctly. The logic
+                // below should only remain as a last-resort catch-all fix.
+                final UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+                if (umi == null || (umi != null && !umi.exists(userId))) {
+                    // The userId passed is a non-existent user so don't report the event.
+                    Slog.wtf(TAG, "Attempted to report event for non-existent user " + userId
+                            + " (" + event.mPackage + "/" + event.mClass
+                            + " eventType:" + event.mEventType + ")");
+                    return;
+                }
                 events = new LinkedList<>();
                 mReportedEvents.put(userId, events);
             }
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 09cb464..61698db 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -9981,6 +9981,18 @@
             "carrier_roaming_satellite_default_services_int_array";
 
     /**
+     * Indicate whether carrier roaming to satellite is using ESOS (Emergency SOS) which connects
+     * to an emergency provider instead of PSAP (Public Safety Answering Point) for emergency
+     * messaging.
+     *
+     * This will need agreement with carriers before enabling this flag.
+     *
+     * The default value is false.
+     */
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    public static final String KEY_SATELLITE_ESOS_SUPPORTED_BOOL = "satellite_esos_supported_bool";
+
+    /**
      * Indicating whether DUN APN should be disabled when the device is roaming. In that case,
      * the default APN (i.e. internet) will be used for tethering.
      *
@@ -11137,6 +11149,7 @@
         sDefaults.putBoolean(KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL, false);
         sDefaults.putInt(KEY_EMERGENCY_CALL_TO_SATELLITE_T911_HANDOVER_TIMEOUT_MILLIS_INT,
                 (int) TimeUnit.SECONDS.toMillis(30));
+        sDefaults.putBoolean(KEY_SATELLITE_ESOS_SUPPORTED_BOOL, false);
         sDefaults.putString(KEY_DEFAULT_PREFERRED_APN_NAME_STRING, "");
         sDefaults.putBoolean(KEY_SUPPORTS_CALL_COMPOSER_BOOL, false);
         sDefaults.putBoolean(KEY_SUPPORTS_BUSINESS_CALL_COMPOSER_BOOL, false);
diff --git a/telephony/java/android/telephony/SubscriptionInfo.java b/telephony/java/android/telephony/SubscriptionInfo.java
index 58488d1..1089602 100644
--- a/telephony/java/android/telephony/SubscriptionInfo.java
+++ b/telephony/java/android/telephony/SubscriptionInfo.java
@@ -274,6 +274,11 @@
     private final int mServiceCapabilities;
 
     /**
+     * Whether the carrier roaming to satellite is using ESOS for emergency messaging.
+     */
+    private final boolean mIsSatelliteESOSSupported;
+
+    /**
      * @hide
      *
      * @deprecated Use {@link SubscriptionInfo.Builder}.
@@ -400,6 +405,7 @@
         this.mIsOnlyNonTerrestrialNetwork = false;
         this.mServiceCapabilities = 0;
         this.mTransferStatus = 0;
+        this.mIsSatelliteESOSSupported = false;
     }
 
     /**
@@ -441,6 +447,7 @@
         this.mIsOnlyNonTerrestrialNetwork = builder.mIsOnlyNonTerrestrialNetwork;
         this.mServiceCapabilities = builder.mServiceCapabilities;
         this.mTransferStatus = builder.mTransferStatus;
+        this.mIsSatelliteESOSSupported = builder.mIsSatelliteESOSSupported;
     }
 
     /**
@@ -898,6 +905,19 @@
         return mIsOnlyNonTerrestrialNetwork;
     }
 
+
+    /**
+     * Checks if the subscription is supported ESOS over Carrier Roaming NB-IOT Satellite.
+     *
+     * @return {@code true} if the subscription supports ESOS over Carrier Roaming NB-IOT Satellite,
+     * {@code false} otherwise.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    public boolean isSatelliteESOSSupported() {
+        return mIsSatelliteESOSSupported;
+    }
+
     // TODO(b/316183370): replace @code with @link in javadoc after feature is released
     /**
      * Retrieves the service capabilities for the current subscription.
@@ -989,6 +1009,7 @@
                     .setServiceCapabilities(
                             SubscriptionManager.getServiceCapabilitiesSet(source.readInt()))
                     .setTransferStatus(source.readInt())
+                    .setSatelliteESOSSupported(source.readBoolean())
                     .build();
         }
 
@@ -1033,6 +1054,7 @@
         dest.writeBoolean(mIsOnlyNonTerrestrialNetwork);
         dest.writeInt(mServiceCapabilities);
         dest.writeInt(mTransferStatus);
+        dest.writeBoolean(mIsSatelliteESOSSupported);
     }
 
     @Override
@@ -1099,6 +1121,7 @@
                 + " serviceCapabilities=" + SubscriptionManager.getServiceCapabilitiesSet(
                 mServiceCapabilities).toString()
                 + " transferStatus=" + mTransferStatus
+                + " isSatelliteESOSSupported=" + mIsSatelliteESOSSupported
                 + "]";
     }
 
@@ -1126,7 +1149,8 @@
                 && mCountryIso.equals(that.mCountryIso) && mGroupOwner.equals(that.mGroupOwner)
                 && mIsOnlyNonTerrestrialNetwork == that.mIsOnlyNonTerrestrialNetwork
                 && mServiceCapabilities == that.mServiceCapabilities
-                && mTransferStatus == that.mTransferStatus;
+                && mTransferStatus == that.mTransferStatus
+                && mIsSatelliteESOSSupported == that.mIsSatelliteESOSSupported;
     }
 
     @Override
@@ -1136,7 +1160,7 @@
                 mCardString, mIsOpportunistic, mGroupUuid, mCountryIso, mCarrierId, mProfileClass,
                 mType, mGroupOwner, mAreUiccApplicationsEnabled, mPortIndex, mUsageSetting, mCardId,
                 mIsGroupDisabled, mIsOnlyNonTerrestrialNetwork, mServiceCapabilities,
-                mTransferStatus);
+                mTransferStatus, mIsSatelliteESOSSupported);
         result = 31 * result + Arrays.hashCode(mEhplmns);
         result = 31 * result + Arrays.hashCode(mHplmns);
         result = 31 * result + Arrays.hashCode(mNativeAccessRules);
@@ -1346,6 +1370,11 @@
          * Service capabilities bitmasks the subscription supports.
          */
         private int mServiceCapabilities = 0;
+        /**
+         * {@code true} if the subscription supports ESOS over Carrier Roaming NB-IOT Satellite.
+         * {@code false} otherwise.
+         */
+        private boolean mIsSatelliteESOSSupported = false;
 
         /**
          * Default constructor.
@@ -1392,6 +1421,7 @@
             mIsOnlyNonTerrestrialNetwork = info.mIsOnlyNonTerrestrialNetwork;
             mServiceCapabilities = info.mServiceCapabilities;
             mTransferStatus = info.mTransferStatus;
+            mIsSatelliteESOSSupported = info.mIsSatelliteESOSSupported;
         }
 
         /**
@@ -1828,6 +1858,21 @@
         }
 
         /**
+         * Set whether the subscription is supported ESOS over Carrier Roaming NB-IOT Satellite or
+         * not.
+         *
+         * @param isSatelliteESOSSupported {@code true} if the subscription supports ESOS over
+         * Carrier Roaming NB-IOT Satellite, {@code false} otherwise.
+         * @return The builder.
+         */
+        @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+        @NonNull
+        public Builder setSatelliteESOSSupported(boolean isSatelliteESOSSupported) {
+            mIsSatelliteESOSSupported = isSatelliteESOSSupported;
+            return this;
+        }
+
+        /**
          * Build the {@link SubscriptionInfo}.
          *
          * @return The {@link SubscriptionInfo} instance.
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 76b4e005..dea10b70 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -1127,7 +1127,7 @@
      * <P>Type: INTEGER (int)</P>
      * @hide
      */
-    public static final String IS_NTN = SimInfo.COLUMN_IS_NTN;
+    public static final String IS_ONLY_NTN = SimInfo.COLUMN_IS_ONLY_NTN;
 
     /**
      * TelephonyProvider column name to identify service capabilities.
@@ -1167,6 +1167,16 @@
     public static final String SATELLITE_ENTITLEMENT_PLMNS =
             SimInfo.COLUMN_SATELLITE_ENTITLEMENT_PLMNS;
 
+    /**
+     * TelephonyProvider column name to indicate the satellite ESOS supported. The value of this
+     * column is set based on {@link CarrierConfigManager#KEY_SATELLITE_ESOS_SUPPORTED_BOOL}.
+     * By default, it's disabled.
+     * <P>Type: INTEGER (int)</P>
+     *
+     * @hide
+     */
+    public static final String SATELLITE_ESOS_SUPPORTED = SimInfo.COLUMN_SATELLITE_ESOS_SUPPORTED;
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = {"USAGE_SETTING_"},
@@ -2783,17 +2793,17 @@
         return phoneId >= 0 && phoneId < TelephonyManager.getDefault().getActiveModemCount();
     }
 
-    /** @hide */
+    /**
+     * Puts phone ID and subscription ID into the {@code intent}.
+     *
+     * <p>If the subscription ID is not valid, only puts phone ID into the {@code intent}.
+     *
+     * @hide
+     */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
     public static void putPhoneIdAndSubIdExtra(Intent intent, int phoneId) {
         int subId = SubscriptionManager.getSubscriptionId(phoneId);
-        if (isValidSubscriptionId(subId)) {
-            putPhoneIdAndSubIdExtra(intent, phoneId, subId);
-        } else {
-            logd("putPhoneIdAndSubIdExtra: no valid subs");
-            intent.putExtra(PhoneConstants.PHONE_KEY, phoneId);
-            intent.putExtra(EXTRA_SLOT_INDEX, phoneId);
-        }
+        putPhoneIdAndMaybeSubIdExtra(intent, phoneId, subId);
     }
 
     /** @hide */
@@ -2806,6 +2816,23 @@
     }
 
     /**
+     * Puts phone ID and subscription ID into the {@code intent}.
+     *
+     * <p>If the subscription ID is not valid, only puts phone ID into the {@code intent}.
+     *
+     * @hide
+     */
+    public static void putPhoneIdAndMaybeSubIdExtra(Intent intent, int phoneId, int subId) {
+        if (isValidSubscriptionId(subId)) {
+            putPhoneIdAndSubIdExtra(intent, phoneId, subId);
+        } else {
+            if (VDBG) logd("putPhoneIdAndMaybeSubIdExtra: invalid subId");
+            intent.putExtra(PhoneConstants.PHONE_KEY, phoneId);
+            intent.putExtra(EXTRA_SLOT_INDEX, phoneId);
+        }
+    }
+
+    /**
      * Get visible subscription Id(s) of the currently active SIM(s).
      *
      * @return the list of subId's that are active,
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 2a359cd..6caed14 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -371,6 +371,24 @@
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_MODEM_TIMEOUT = 24;
 
+    /**
+     * Telephony framework needs to access the current location of the device to perform the
+     * request. However, location in the settings is disabled by users.
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+    public static final int SATELLITE_RESULT_LOCATION_DISABLED = 25;
+
+    /**
+     * Telephony framework needs to access the current location of the device to perform the
+     * request. However, Telephony fails to fetch the current location from location service.
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+    public static final int SATELLITE_RESULT_LOCATION_NOT_AVAILABLE = 26;
+
     /** @hide */
     @IntDef(prefix = {"SATELLITE_RESULT_"}, value = {
             SATELLITE_RESULT_SUCCESS,
@@ -397,7 +415,9 @@
             SATELLITE_RESULT_REQUEST_IN_PROGRESS,
             SATELLITE_RESULT_MODEM_BUSY,
             SATELLITE_RESULT_ILLEGAL_STATE,
-            SATELLITE_RESULT_MODEM_TIMEOUT
+            SATELLITE_RESULT_MODEM_TIMEOUT,
+            SATELLITE_RESULT_LOCATION_DISABLED,
+            SATELLITE_RESULT_LOCATION_NOT_AVAILABLE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface SatelliteResult {}
diff --git a/wifi/java/src/android/net/wifi/WifiMigration.java b/wifi/java/src/android/net/wifi/WifiMigration.java
index 4fabc0b..1a20a12 100644
--- a/wifi/java/src/android/net/wifi/WifiMigration.java
+++ b/wifi/java/src/android/net/wifi/WifiMigration.java
@@ -19,16 +19,23 @@
 import static android.os.Environment.getDataMiscCeDirectory;
 import static android.os.Environment.getDataMiscDirectory;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.content.Context;
+import android.net.wifi.flags.Flags;
+import android.os.Binder;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.Process;
+import android.os.ServiceSpecificException;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.security.legacykeystore.ILegacyKeystore;
 import android.util.AtomicFile;
+import android.util.Log;
 import android.util.SparseArray;
 
 import java.io.File;
@@ -36,7 +43,11 @@
 import java.io.InputStream;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 
 /**
  * Class used to provide one time hooks for existing OEM devices to migrate their config store
@@ -45,6 +56,8 @@
  */
 @SystemApi
 public final class WifiMigration {
+    private static final String TAG = "WifiMigration";
+
     /**
      * Directory to read the wifi config store files from under.
      */
@@ -555,4 +568,49 @@
         return data;
 
     }
+
+    /**
+     * Migrate any certificates in Legacy Keystore to the newer WifiBlobstore database.
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_LEGACY_KEYSTORE_TO_WIFI_BLOBSTORE_MIGRATION)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static void migrateLegacyKeystoreToWifiBlobstore() {
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            ILegacyKeystore legacyKeystore = WifiBlobStore.getLegacyKeystore();
+            String[] legacyAliases = legacyKeystore.list("", Process.WIFI_UID);
+            if (legacyAliases == null || legacyAliases.length == 0) {
+                Log.i(TAG, "No aliases need to be migrated");
+                return;
+            }
+
+            WifiBlobStore wifiBlobStore = WifiBlobStore.getInstance();
+            List<String> blobstoreAliasList = Arrays.asList(wifiBlobStore.list(""));
+            Set<String> blobstoreAliases = new HashSet<>();
+            blobstoreAliases.addAll(blobstoreAliasList);
+
+            for (String legacyAlias : legacyAliases) {
+                // Only migrate if the alias is not already in WifiBlobstore,
+                // since WifiBlobstore should already contain the latest value.
+                if (!blobstoreAliases.contains(legacyAlias)) {
+                    byte[] value = legacyKeystore.get(legacyAlias, Process.WIFI_UID);
+                    wifiBlobStore.put(legacyAlias, value);
+                }
+                legacyKeystore.remove(legacyAlias, Process.WIFI_UID);
+            }
+            Log.i(TAG, "Successfully migrated aliases from Legacy Keystore");
+        } catch (ServiceSpecificException e) {
+            if (e.errorCode == ILegacyKeystore.ERROR_SYSTEM_ERROR) {
+                Log.i(TAG, "Legacy Keystore service has been deprecated");
+            } else {
+                Log.e(TAG, "Encountered an exception while migrating aliases. " + e);
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Encountered an exception while migrating aliases. " + e);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
 }
diff --git a/wifi/tests/src/android/net/wifi/WifiMigrationTest.java b/wifi/tests/src/android/net/wifi/WifiMigrationTest.java
new file mode 100644
index 0000000..8a5912f
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/WifiMigrationTest.java
@@ -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 android.net.wifi;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+import android.security.legacykeystore.ILegacyKeystore;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+
+/**
+ * Unit tests for {@link WifiMigration}.
+ */
+public class WifiMigrationTest {
+    public static final String TEST_ALIAS = "someAliasString";
+    public static final byte[] TEST_VALUE = new byte[]{10, 11, 12};
+
+    @Mock private ILegacyKeystore mLegacyKeystore;
+    @Mock private WifiBlobStore mWifiBlobStore;
+
+    private MockitoSession mSession;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mSession = ExtendedMockito.mockitoSession()
+                .mockStatic(WifiBlobStore.class, withSettings().lenient())
+                .startMocking();
+        when(WifiBlobStore.getLegacyKeystore()).thenReturn(mLegacyKeystore);
+        when(WifiBlobStore.getInstance()).thenReturn(mWifiBlobStore);
+        when(mLegacyKeystore.get(anyString(), anyInt())).thenReturn(TEST_VALUE);
+    }
+
+    @After
+    public void cleanup() {
+        validateMockitoUsage();
+        if (mSession != null) {
+            mSession.finishMocking();
+        }
+    }
+
+    /**
+     * Verify that the Keystore migration method returns immediately if no aliases
+     * are found in Legacy Keystore.
+     */
+    @Test
+    public void testKeystoreMigrationNoLegacyAliases() throws Exception {
+        when(mLegacyKeystore.list(anyString(), anyInt())).thenReturn(new String[0]);
+        WifiMigration.migrateLegacyKeystoreToWifiBlobstore();
+        verify(mLegacyKeystore).list(anyString(), anyInt());
+        verifyNoMoreInteractions(mLegacyKeystore, mWifiBlobStore);
+    }
+
+    /**
+     * Verify that if all aliases in Legacy Keystore are unique to that database,
+     * all aliases are migrated to WifiBlobstore.
+     */
+    @Test
+    public void testKeystoreMigrationUniqueLegacyAliases() throws Exception {
+        String[] legacyAliases = new String[]{TEST_ALIAS + "1", TEST_ALIAS + "2"};
+        String[] blobstoreAliases = new String[0];
+        when(mLegacyKeystore.list(anyString(), anyInt())).thenReturn(legacyAliases);
+        when(mWifiBlobStore.list(anyString())).thenReturn(blobstoreAliases);
+
+        WifiMigration.migrateLegacyKeystoreToWifiBlobstore();
+        verify(mWifiBlobStore, times(legacyAliases.length)).put(anyString(), any(byte[].class));
+    }
+
+    /**
+     * Verify that if some aliases are shared between Legacy Keystore and WifiBlobstore,
+     * only the ones unique to Legacy Keystore are migrated.
+     */
+    @Test
+    public void testKeystoreMigrationDuplicateLegacyAliases() throws Exception {
+        String uniqueLegacyAlias = TEST_ALIAS + "1";
+        String[] blobstoreAliases = new String[]{TEST_ALIAS + "2", TEST_ALIAS + "3"};
+        String[] legacyAliases =
+                new String[]{blobstoreAliases[0], blobstoreAliases[1], uniqueLegacyAlias};
+        when(mLegacyKeystore.list(anyString(), anyInt())).thenReturn(legacyAliases);
+        when(mWifiBlobStore.list(anyString())).thenReturn(blobstoreAliases);
+
+        // Expect that only the unique legacy alias is migrated to the blobstore
+        WifiMigration.migrateLegacyKeystoreToWifiBlobstore();
+        verify(mWifiBlobStore).list(anyString());
+        verify(mWifiBlobStore).put(eq(uniqueLegacyAlias), any(byte[].class));
+        verifyNoMoreInteractions(mWifiBlobStore);
+    }
+}
diff --git a/wifi/wifi.aconfig b/wifi/wifi.aconfig
index c5bc039..c1effe1 100644
--- a/wifi/wifi.aconfig
+++ b/wifi/wifi.aconfig
@@ -17,3 +17,11 @@
     description: "Control the API that allows setting / reading the NetworkProviderInfo's battery charging status"
     bug: "305067231"
 }
+
+flag {
+    name: "legacy_keystore_to_wifi_blobstore_migration"
+    is_exported: true
+    namespace: "wifi"
+    description: "Add API to migrate all values from Legacy Keystore to the new Wifi Blobstore database"
+    bug: "332560152"
+}