Merge "Zero jank IMF 4/N" into main
diff --git a/apex/jobscheduler/service/Android.bp b/apex/jobscheduler/service/Android.bp
index 8b55e07..5586295 100644
--- a/apex/jobscheduler/service/Android.bp
+++ b/apex/jobscheduler/service/Android.bp
@@ -28,6 +28,7 @@
 
     static_libs: [
         "modules-utils-fastxmlserializer",
+        "service-jobscheduler-alarm.flags-aconfig-java",
         "service-jobscheduler-job.flags-aconfig-java",
     ],
 
diff --git a/apex/jobscheduler/service/aconfig/Android.bp b/apex/jobscheduler/service/aconfig/Android.bp
index 3ba7aa2..4db39dc 100644
--- a/apex/jobscheduler/service/aconfig/Android.bp
+++ b/apex/jobscheduler/service/aconfig/Android.bp
@@ -27,3 +27,15 @@
     aconfig_declarations: "service-job.flags-aconfig",
     visibility: ["//frameworks/base:__subpackages__"],
 }
+
+// Alarm
+aconfig_declarations {
+    name: "alarm_flags",
+    package: "com.android.server.alarm",
+    srcs: ["alarm.aconfig"],
+}
+
+java_aconfig_library {
+    name: "service-jobscheduler-alarm.flags-aconfig-java",
+    aconfig_declarations: "alarm_flags",
+}
diff --git a/apex/jobscheduler/service/aconfig/alarm.aconfig b/apex/jobscheduler/service/aconfig/alarm.aconfig
new file mode 100644
index 0000000..3b9b4e7
--- /dev/null
+++ b/apex/jobscheduler/service/aconfig/alarm.aconfig
@@ -0,0 +1,11 @@
+package: "com.android.server.alarm"
+
+flag {
+    name: "use_frozen_state_to_drop_listener_alarms"
+    namespace: "backstage_power"
+    description: "Use frozen state callback to drop listener alarms for cached apps"
+    bug: "324470945"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 39de0af..e728a2c 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.alarm;
 
+import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN;
 import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
 import static android.app.AlarmManager.ELAPSED_REALTIME;
 import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
@@ -75,6 +76,7 @@
 import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
 import android.app.Activity;
+import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.ActivityOptions;
 import android.app.AlarmManager;
@@ -103,6 +105,7 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
@@ -145,6 +148,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IAppOpsCallback;
 import com.android.internal.app.IAppOpsService;
+import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.LocalLog;
@@ -293,6 +297,7 @@
 
     private final Injector mInjector;
     int mBroadcastRefCount = 0;
+    boolean mUseFrozenStateToDropListenerAlarms;
     MetricsHelper mMetricsHelper;
     PowerManager.WakeLock mWakeLock;
     SparseIntArray mAlarmsPerUid = new SparseIntArray();
@@ -1856,15 +1861,47 @@
     @Override
     public void onStart() {
         mInjector.init();
+        mHandler = new AlarmHandler();
+
         mOptsWithFgs.setPendingIntentBackgroundActivityLaunchAllowed(false);
         mOptsWithFgsForAlarmClock.setPendingIntentBackgroundActivityLaunchAllowed(false);
         mOptsWithoutFgs.setPendingIntentBackgroundActivityLaunchAllowed(false);
         mOptsTimeBroadcast.setPendingIntentBackgroundActivityLaunchAllowed(false);
         mActivityOptsRestrictBal.setPendingIntentBackgroundActivityLaunchAllowed(false);
         mBroadcastOptsRestrictBal.setPendingIntentBackgroundActivityLaunchAllowed(false);
+
         mMetricsHelper = new MetricsHelper(getContext(), mLock);
         mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
 
+        mUseFrozenStateToDropListenerAlarms = Flags.useFrozenStateToDropListenerAlarms();
+        if (mUseFrozenStateToDropListenerAlarms) {
+            final ActivityManager.UidFrozenStateChangedCallback callback = (uids, frozenStates) -> {
+                final int size = frozenStates.length;
+                if (uids.length != size) {
+                    Slog.wtf(TAG, "Got different length arrays in frozen state callback!"
+                            + " uids.length: " + uids.length + " frozenStates.length: " + size);
+                    // Cannot process received data in any meaningful way.
+                    return;
+                }
+                final IntArray affectedUids = new IntArray();
+                for (int i = 0; i < size; i++) {
+                    if (frozenStates[i] != UID_FROZEN_STATE_FROZEN) {
+                        continue;
+                    }
+                    if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED,
+                            uids[i])) {
+                        continue;
+                    }
+                    affectedUids.add(uids[i]);
+                }
+                if (affectedUids.size() > 0) {
+                    removeExactListenerAlarms(affectedUids.toArray());
+                }
+            };
+            final ActivityManager am = getContext().getSystemService(ActivityManager.class);
+            am.registerUidFrozenStateChangedCallback(new HandlerExecutor(mHandler), callback);
+        }
+
         mListenerDeathRecipient = new IBinder.DeathRecipient() {
             @Override
             public void binderDied() {
@@ -1880,7 +1917,6 @@
         };
 
         synchronized (mLock) {
-            mHandler = new AlarmHandler();
             mConstants = new Constants(mHandler);
 
             mAlarmStore = new LazyAlarmStore();
@@ -1960,6 +1996,21 @@
         publishBinderService(Context.ALARM_SERVICE, mService);
     }
 
+    private void removeExactListenerAlarms(int... whichUids) {
+        synchronized (mLock) {
+            removeAlarmsInternalLocked(a -> {
+                if (!ArrayUtils.contains(whichUids, a.uid) || a.listener == null
+                        || a.windowLength != 0) {
+                    return false;
+                }
+                Slog.w(TAG, "Alarm " + a.listenerTag + " being removed for "
+                        + UserHandle.formatUid(a.uid) + ":" + a.packageName
+                        + " because the app got frozen");
+                return true;
+            }, REMOVE_REASON_LISTENER_CACHED);
+        }
+    }
+
     void refreshExactAlarmCandidates() {
         final String[] candidates = mLocalPermissionManager.getAppOpPermissionPackages(
                 Manifest.permission.SCHEDULE_EXACT_ALARM);
@@ -3074,6 +3125,14 @@
             mConstants.dump(pw);
             pw.println();
 
+            pw.println("Feature Flags:");
+            pw.increaseIndent();
+            pw.print(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS,
+                    mUseFrozenStateToDropListenerAlarms);
+            pw.decreaseIndent();
+            pw.println();
+            pw.println();
+
             if (mConstants.USE_TARE_POLICY == EconomyManager.ENABLED_MODE_ON) {
                 pw.println("TARE details:");
                 pw.increaseIndent();
@@ -4959,18 +5018,7 @@
                     break;
                 case REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED:
                     uid = (Integer) msg.obj;
-                    synchronized (mLock) {
-                        removeAlarmsInternalLocked(a -> {
-                            if (a.uid != uid || a.listener == null || a.windowLength != 0) {
-                                return false;
-                            }
-                            // TODO (b/265195908): Change to .w once we have some data on breakages.
-                            Slog.wtf(TAG, "Alarm " + a.listenerTag + " being removed for "
-                                    + UserHandle.formatUid(a.uid) + ":" + a.packageName
-                                    + " because the app went into cached state");
-                            return true;
-                        }, REMOVE_REASON_LISTENER_CACHED);
-                    }
+                    removeExactListenerAlarms(uid);
                     break;
                 default:
                     // nope, just ignore it
@@ -5322,6 +5370,10 @@
 
         @Override
         public void handleUidCachedChanged(int uid, boolean cached) {
+            if (mUseFrozenStateToDropListenerAlarms) {
+                // Use ActivityManager#UidFrozenStateChangedCallback instead.
+                return;
+            }
             if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, uid)) {
                 return;
             }
diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java
index d139134..fb1b17b 100644
--- a/core/java/android/app/assist/AssistStructure.java
+++ b/core/java/android/app/assist/AssistStructure.java
@@ -10,6 +10,7 @@
 import android.app.Activity;
 import android.content.ComponentName;
 import android.content.Context;
+import android.credentials.CredentialOption;
 import android.credentials.GetCredentialException;
 import android.credentials.GetCredentialRequest;
 import android.credentials.GetCredentialResponse;
@@ -29,6 +30,7 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.service.autofill.FillRequest;
+import android.service.credentials.CredentialProviderService;
 import android.text.InputType;
 import android.text.Spanned;
 import android.text.TextUtils;
@@ -2258,6 +2260,17 @@
                 @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) {
             mNode.mGetCredentialRequest = request;
             mNode.mGetCredentialCallback = callback;
+            for (CredentialOption option : request.getCredentialOptions()) {
+                ArrayList<AutofillId> ids = option.getCandidateQueryData()
+                        .getParcelableArrayList(
+                                CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId.class);
+                ids = ids != null ? ids : new ArrayList<>();
+                if (!ids.contains(getAutofillId())) {
+                    ids.add(getAutofillId());
+                }
+                option.getCandidateQueryData()
+                        .putParcelableArrayList(CredentialProviderService.EXTRA_AUTOFILL_ID, ids);
+            }
         }
 
         @Override
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 028448c..06dc275 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -85,6 +85,7 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.credentials.CredentialManager;
+import android.credentials.CredentialOption;
 import android.credentials.GetCredentialException;
 import android.credentials.GetCredentialRequest;
 import android.credentials.GetCredentialResponse;
@@ -129,6 +130,7 @@
 import android.os.Trace;
 import android.os.Vibrator;
 import android.os.vibrator.Flags;
+import android.service.credentials.CredentialProviderService;
 import android.sysprop.DisplayProperties;
 import android.text.InputType;
 import android.text.TextUtils;
@@ -7033,6 +7035,18 @@
             @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) {
         Preconditions.checkNotNull(request, "request must not be null");
         Preconditions.checkNotNull(callback, "request must not be null");
+
+        for (CredentialOption option : request.getCredentialOptions()) {
+            ArrayList<AutofillId> ids = option.getCandidateQueryData()
+                    .getParcelableArrayList(
+                            CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId.class);
+            ids = ids != null ? ids : new ArrayList<>();
+            if (!ids.contains(getAutofillId())) {
+                ids.add(getAutofillId());
+            }
+            option.getCandidateQueryData()
+                    .putParcelableArrayList(CredentialProviderService.EXTRA_AUTOFILL_ID, ids);
+        }
         mViewCredentialHandler = new ViewCredentialHandler(request, callback);
     }
 
diff --git a/core/java/android/view/inputmethod/InputBinding.java b/core/java/android/view/inputmethod/InputBinding.java
index 2bfeb5a..fedee9d 100644
--- a/core/java/android/view/inputmethod/InputBinding.java
+++ b/core/java/android/view/inputmethod/InputBinding.java
@@ -19,11 +19,13 @@
 import android.os.IBinder;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
 
 /**
  * Information given to an {@link InputMethod} about a client connecting
  * to it.
  */
+@RavenwoodKeepWholeClass
 public final class InputBinding implements Parcelable {
     static final String TAG = "InputBinding";
     
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 34be9b0..2606fb6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -114,6 +114,7 @@
     /** Tracks if we should start the back gesture on the next motion move event */
     private boolean mShouldStartOnNextMoveEvent = false;
     private boolean mOnBackStartDispatched = false;
+    private boolean mPointerPilfered = false;
 
     private final FlingAnimationUtils mFlingAnimationUtils;
 
@@ -404,11 +405,12 @@
 
     @VisibleForTesting
     void onPilferPointers() {
+        mPointerPilfered = true;
         // Dispatch onBackStarted, only to app callbacks.
         // System callbacks will receive onBackStarted when the remote animation starts.
         if (!shouldDispatchToAnimator() && mActiveCallback != null) {
             mCurrentTracker.updateStartLocation();
-            tryDispatchAppOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null));
+            tryDispatchOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null));
         }
     }
 
@@ -511,7 +513,7 @@
             mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback();
             // App is handling back animation. Cancel system animation latency tracking.
             cancelLatencyTracking();
-            tryDispatchAppOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null));
+            tryDispatchOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null));
         }
     }
 
@@ -555,14 +557,13 @@
                 && mBackNavigationInfo.isPrepareRemoteAnimation();
     }
 
-    private void tryDispatchAppOnBackStarted(
+    private void tryDispatchOnBackStarted(
             IOnBackInvokedCallback callback,
             BackMotionEvent backEvent) {
-        if (mOnBackStartDispatched && callback != null) {
+        if (mOnBackStartDispatched || callback == null || !mPointerPilfered) {
             return;
         }
         dispatchOnBackStarted(callback, backEvent);
-        mOnBackStartDispatched = true;
     }
 
     private void dispatchOnBackStarted(
@@ -573,6 +574,7 @@
         }
         try {
             callback.onBackStarted(backEvent);
+            mOnBackStartDispatched = true;
         } catch (RemoteException e) {
             Log.e(TAG, "dispatchOnBackStarted error: ", e);
         }
@@ -872,6 +874,7 @@
         mActiveCallback = null;
         mShouldStartOnNextMoveEvent = false;
         mOnBackStartDispatched = false;
+        mPointerPilfered = false;
         mShellBackAnimationRegistry.resetDefaultCrossActivity();
         cancelLatencyTracking();
         if (mBackNavigationInfo != null) {
@@ -957,15 +960,7 @@
                                         mCurrentTracker.updateStartLocation();
                                         BackMotionEvent startEvent =
                                                 mCurrentTracker.createStartEvent(apps[0]);
-                                        // {@code mActiveCallback} is the callback from
-                                        // the BackAnimationRunners and not a real app-side
-                                        // callback. We also dispatch to the app-side callback
-                                        // (which should be a system callback with PRIORITY_SYSTEM)
-                                        // to keep consistent with app registered callbacks.
                                         dispatchOnBackStarted(mActiveCallback, startEvent);
-                                        tryDispatchAppOnBackStarted(
-                                                mBackNavigationInfo.getOnBackInvokedCallback(),
-                                                startEvent);
                                     }
 
                                     // Dispatch the first progress after animation start for
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index ef32f6f5..c7daf56 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -1996,6 +1996,7 @@
         pw.println(innerPrefix + "mToken=" + mToken
                 + " binder=" + (mToken != null ? mToken.asBinder() : null));
         pw.println(innerPrefix + "mLeash=" + mLeash);
+        pw.println(innerPrefix + "mPipOverlay=" + mPipOverlay);
         pw.println(innerPrefix + "mState=" + mPipTransitionState.getTransitionState());
         pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams);
         mPipTransitionController.dump(pw, innerPrefix);
diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
index 5242a7d..c81b95b 100644
--- a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
+++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
@@ -176,11 +176,25 @@
             List<AidGroup> staticAidGroups, List<AidGroup> dynamicAidGroups,
             boolean requiresUnlock, boolean requiresScreenOn, int bannerResource, int uid,
             String settingsActivityName, String offHost, String staticOffHost, boolean isEnabled) {
+        this(info, onHost, description, staticAidGroups, dynamicAidGroups,
+                requiresUnlock, requiresScreenOn, bannerResource, uid,
+                settingsActivityName, offHost, staticOffHost, isEnabled,
+                new HashMap<String, Boolean>());
+    }
+
+    /**
+     * @hide
+     */
+    public ApduServiceInfo(ResolveInfo info, boolean onHost, String description,
+            List<AidGroup> staticAidGroups, List<AidGroup> dynamicAidGroups,
+            boolean requiresUnlock, boolean requiresScreenOn, int bannerResource, int uid,
+            String settingsActivityName, String offHost, String staticOffHost, boolean isEnabled,
+            HashMap<String, Boolean> autoTransact) {
         this.mService = info;
         this.mDescription = description;
         this.mStaticAidGroups = new HashMap<String, AidGroup>();
         this.mDynamicAidGroups = new HashMap<String, AidGroup>();
-        this.mAutoTransact = new HashMap<String, Boolean>();
+        this.mAutoTransact = autoTransact;
         this.mOffHostName = offHost;
         this.mStaticOffHostName = staticOffHost;
         this.mOnHost = onHost;
@@ -196,7 +210,6 @@
         this.mUid = uid;
         this.mSettingsActivityName = settingsActivityName;
         this.mCategoryOtherServiceEnabled = isEnabled;
-
     }
 
     /**
@@ -857,6 +870,8 @@
         dest.writeString(mSettingsActivityName);
 
         dest.writeInt(mCategoryOtherServiceEnabled ? 1 : 0);
+        dest.writeInt(mAutoTransact.size());
+        dest.writeMap(mAutoTransact);
     };
 
     @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE)
@@ -885,10 +900,15 @@
                     int uid = source.readInt();
                     String settingsActivityName = source.readString();
                     boolean isEnabled = source.readInt() != 0;
+                    int autoTransactSize = source.readInt();
+                    HashMap<String, Boolean> autoTransact =
+                            new HashMap<String, Boolean>(autoTransactSize);
+                    source.readMap(autoTransact, getClass().getClassLoader(),
+                            String.class, Boolean.class);
                     return new ApduServiceInfo(info, onHost, description, staticAidGroups,
                             dynamicAidGroups, requiresUnlock, requiresScreenOn, bannerResource, uid,
                             settingsActivityName, offHostName, staticOffHostName,
-                            isEnabled);
+                            isEnabled, autoTransact);
                 }
 
                 @Override
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
index 5599ccd..eef75c7 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
@@ -40,6 +40,7 @@
 import android.service.autofill.FillCallback
 import android.service.autofill.FillRequest
 import android.service.autofill.FillResponse
+import android.service.autofill.Flags
 import android.service.autofill.InlinePresentation
 import android.service.autofill.Presentations
 import android.service.autofill.SaveCallback
@@ -250,7 +251,7 @@
         maxInlineItemCount = maxInlineItemCount.coerceAtMost(inlineMaxSuggestedCount)
         val lastDropdownDatasetIndex = Settings.Global.getInt(this.contentResolver,
                 Settings.Global.AUTOFILL_MAX_VISIBLE_DATASETS,
-                (maxDropdownDisplayLimit - 1)).coerceAtMost(totalEntryCount - 1)
+                (maxDropdownDisplayLimit - 1)).coerceAtMost(totalEntryCount)
 
         var i = 0
         var datasetAdded = false
@@ -479,18 +480,28 @@
         val autofillIdToCredentialEntries:
                 MutableMap<AutofillId, ArrayList<Entry>> = mutableMapOf()
         credentialEntryList.forEach entryLoop@{ credentialEntry ->
-            val autofillId: AutofillId? = credentialEntry
-                    .frameworkExtrasIntent
-                    ?.getParcelableExtra(
-                            CredentialProviderService.EXTRA_AUTOFILL_ID,
-                            AutofillId::class.java)
-            if (autofillId == null) {
-                Log.e(TAG, "AutofillId is missing from credential entry. Credential" +
-                        " Integration might be disabled.")
-                return@entryLoop
-            }
-            autofillIdToCredentialEntries.getOrPut(autofillId) { ArrayList() }
-                    .add(credentialEntry)
+            val intent = credentialEntry.frameworkExtrasIntent
+            intent?.getParcelableExtra(
+                        CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST,
+                        android.service.credentials.GetCredentialRequest::class.java)
+                    ?.credentialOptions
+                    ?.forEach { credentialOption ->
+                        credentialOption.candidateQueryData.getParcelableArrayList(
+                            CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId::class.java)
+                                ?.forEach { autofillId ->
+                                    intent.putExtra(
+                                        CredentialProviderService.EXTRA_AUTOFILL_ID,
+                                        autofillId)
+                                    val entry = Entry(
+                                        credentialEntry.key,
+                                        credentialEntry.subkey,
+                                        credentialEntry.slice,
+                                        intent)
+                                    autofillIdToCredentialEntries
+                                            .getOrPut(autofillId) { ArrayList() }
+                                            .add(entry)
+                                }
+                    }
         }
         return autofillIdToCredentialEntries
     }
@@ -573,23 +584,31 @@
             cmRequests: MutableList<CredentialOption>,
             responseClientState: Bundle
     ) {
+        val traversedViewNodes: MutableSet<AutofillId> = mutableSetOf()
+        val credentialOptionsFromHints: MutableMap<String, CredentialOption> = mutableMapOf()
         val windowNodes: List<AssistStructure.WindowNode> =
                 structure.run {
                     (0 until windowNodeCount).map { getWindowNodeAt(it) }
                 }
 
         windowNodes.forEach { windowNode: AssistStructure.WindowNode ->
-            traverseNodeForRequest(windowNode.rootViewNode, cmRequests, responseClientState)
+            traverseNodeForRequest(
+                windowNode.rootViewNode, cmRequests, responseClientState, traversedViewNodes,
+                credentialOptionsFromHints)
         }
     }
 
     private fun traverseNodeForRequest(
             viewNode: AssistStructure.ViewNode,
             cmRequests: MutableList<CredentialOption>,
-            responseClientState: Bundle
+            responseClientState: Bundle,
+            traversedViewNodes: MutableSet<AutofillId>,
+            credentialOptionsFromHints: MutableMap<String, CredentialOption>
     ) {
         viewNode.autofillId?.let {
-            cmRequests.addAll(getCredentialOptionsFromViewNode(viewNode, it, responseClientState))
+            cmRequests.addAll(getCredentialOptionsFromViewNode(viewNode, it, responseClientState,
+                traversedViewNodes, credentialOptionsFromHints))
+            traversedViewNodes.add(it)
         }
 
         val children: List<AssistStructure.ViewNode> =
@@ -598,26 +617,37 @@
                 }
 
         children.forEach { childNode: AssistStructure.ViewNode ->
-            traverseNodeForRequest(childNode, cmRequests, responseClientState)
+            traverseNodeForRequest(childNode, cmRequests, responseClientState, traversedViewNodes,
+                credentialOptionsFromHints)
         }
     }
 
     private fun getCredentialOptionsFromViewNode(
             viewNode: AssistStructure.ViewNode,
             autofillId: AutofillId,
-            responseClientState: Bundle
+            responseClientState: Bundle,
+            traversedViewNodes: MutableSet<AutofillId>,
+            credentialOptionsFromHints: MutableMap<String, CredentialOption>
     ): MutableList<CredentialOption> {
-        if (viewNode.credentialManagerRequest != null) {
-            val options = viewNode.credentialManagerRequest?.getCredentialOptions()
-            if (options != null) {
-                for (option in options) {
-                    option.candidateQueryData.putParcelable(
-                            CredentialProviderService.EXTRA_AUTOFILL_ID, autofillId
-                    )
-                }
-                return options
+        val credentialOptions: MutableList<CredentialOption> = mutableListOf()
+        if (Flags.autofillCredmanDevIntegration() && viewNode.credentialManagerRequest != null) {
+            viewNode.credentialManagerRequest
+                    ?.getCredentialOptions()
+                    ?.forEach { credentialOption ->
+                credentialOption.candidateQueryData
+                        .getParcelableArrayList(
+                            CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId::class.java)
+                        ?.let { associatedAutofillIds ->
+                            // Check whether any of the associated autofill ids have already been
+                            // traversed. If so, skip, to dedupe on duplicate credential options.
+                            if ((traversedViewNodes intersect associatedAutofillIds.toSet())
+                                        .isEmpty()) {
+                                credentialOptions.add(credentialOption)
+                            }
+                        }
             }
         }
+        // TODO(b/325502552): clean up cred option logic in autofill hint
         val credentialHints: MutableList<String> = mutableListOf()
 
         if (viewNode.autofillHints != null) {
@@ -631,10 +661,10 @@
             }
         }
 
-        val credentialOptions: MutableList<CredentialOption> = mutableListOf()
         for (credentialHint in credentialHints) {
             try {
-                convertJsonToCredentialOption(credentialHint, autofillId)
+                convertJsonToCredentialOption(
+                    credentialHint, autofillId, credentialOptionsFromHints)
                         .let { credentialOptions.addAll(it) }
             } catch (e: JSONException) {
                 Log.i(TAG, "Exception while parsing response: " + e.message)
@@ -643,10 +673,11 @@
         return credentialOptions
     }
 
-    private fun convertJsonToCredentialOption(jsonString: String, autofillId: AutofillId):
-            List<CredentialOption> {
-        // TODO(b/302000646) Move this logic to jetpack so that is consistent
-        //  with building the json
+    private fun convertJsonToCredentialOption(
+        jsonString: String,
+        autofillId: AutofillId,
+        credentialOptionsFromHints: MutableMap<String, CredentialOption>
+    ): List<CredentialOption> {
         val credentialOptions: MutableList<CredentialOption> = mutableListOf()
 
         val json = JSONObject(jsonString)
@@ -654,16 +685,34 @@
         val options = jsonGet.getJSONArray(CRED_OPTIONS_KEY)
         for (i in 0 until options.length()) {
             val option = options.getJSONObject(i)
-            val candidateBundle = convertJsonToBundle(option.getJSONObject(CANDIDATE_DATA_KEY))
-            candidateBundle.putParcelable(
+            val optionString = option.toString()
+            credentialOptionsFromHints[optionString]
+                    ?.let { credentialOption ->
+                        // if the current credential option was seen before, add the current
+                        // viewNode to the credential option, but do not add it to the option list
+                        // again. This will result in the same result as deduping based on
+                        // traversed viewNode.
+                        credentialOption.candidateQueryData.getParcelableArrayList(
+                            CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId::class.java)
+                                ?.let {
+                                    it.add(autofillId)
+                                    credentialOption.candidateQueryData.putParcelableArrayList(
+                                        CredentialProviderService.EXTRA_AUTOFILL_ID, it)
+                                }
+            } ?: run {
+                val candidateBundle = convertJsonToBundle(option.getJSONObject(CANDIDATE_DATA_KEY))
+                candidateBundle.putParcelableArrayList(
                     CredentialProviderService.EXTRA_AUTOFILL_ID,
-                    autofillId)
-            credentialOptions.add(CredentialOption(
+                    arrayListOf(autofillId))
+                val credentialOption = CredentialOption(
                     option.getString(TYPE_KEY),
                     convertJsonToBundle(option.getJSONObject(REQUEST_DATA_KEY)),
                     candidateBundle,
                     option.getBoolean(SYS_PROVIDER_REQ_KEY),
-            ))
+                )
+                credentialOptions.add(credentialOption)
+                credentialOptionsFromHints[optionString] = credentialOption
+            }
         }
         return credentialOptions
     }
diff --git a/packages/PrintRecommendationService/res/values/strings.xml b/packages/PrintRecommendationService/res/values/strings.xml
index 2bab1b6..b6c45b7 100644
--- a/packages/PrintRecommendationService/res/values/strings.xml
+++ b/packages/PrintRecommendationService/res/values/strings.xml
@@ -18,7 +18,6 @@
 -->
 
 <resources>
-    <string name="plugin_vendor_google_cloud_print">Cloud Print</string>
     <string name="plugin_vendor_hp">HP</string>
     <string name="plugin_vendor_lexmark">Lexmark</string>
     <string name="plugin_vendor_brother">Brother</string>
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java
index 5a756fe..4ec8883 100644
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java
+++ b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java
@@ -23,7 +23,6 @@
 import android.printservice.recommendation.RecommendationService;
 import android.util.Log;
 
-import com.android.printservice.recommendation.plugin.google.CloudPrintPlugin;
 import com.android.printservice.recommendation.plugin.hp.HPRecommendationPlugin;
 import com.android.printservice.recommendation.plugin.mdnsFilter.MDNSFilterPlugin;
 import com.android.printservice.recommendation.plugin.mdnsFilter.VendorConfig;
@@ -77,14 +76,6 @@
         }
 
         try {
-            mPlugins.add(new RemotePrintServicePlugin(new CloudPrintPlugin(this), this,
-                    true));
-        } catch (Exception e) {
-            Log.e(LOG_TAG, "Could not initiate "
-                            + getString(R.string.plugin_vendor_google_cloud_print) + " plugin", e);
-        }
-
-        try {
             mPlugins.add(new RemotePrintServicePlugin(new HPRecommendationPlugin(this), this,
                     false));
         } catch (Exception e) {
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/google/CloudPrintPlugin.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/google/CloudPrintPlugin.java
deleted file mode 100644
index 3029d10..0000000
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/google/CloudPrintPlugin.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2017 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.printservice.recommendation.plugin.google;
-
-import static com.android.printservice.recommendation.util.MDNSUtils.ATTRIBUTE_TY;
-
-import android.content.Context;
-import android.util.ArrayMap;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.StringRes;
-
-import com.android.printservice.recommendation.PrintServicePlugin;
-import com.android.printservice.recommendation.R;
-import com.android.printservice.recommendation.util.MDNSFilteredDiscovery;
-
-import java.net.Inet4Address;
-import java.net.InetAddress;
-import java.nio.charset.StandardCharsets;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * Plugin detecting <a href="https://developers.google.com/cloud-print/docs/privet">Google Cloud
- * Print</a> printers.
- */
-public class CloudPrintPlugin implements PrintServicePlugin {
-    private static final String LOG_TAG = CloudPrintPlugin.class.getSimpleName();
-    private static final boolean DEBUG = false;
-
-    private static final String ATTRIBUTE_TXTVERS = "txtvers";
-    private static final String ATTRIBUTE_URL = "url";
-    private static final String ATTRIBUTE_TYPE = "type";
-    private static final String ATTRIBUTE_ID = "id";
-    private static final String ATTRIBUTE_CS = "cs";
-
-    private static final String TYPE = "printer";
-
-    private static final String PRIVET_SERVICE = "_privet._tcp";
-
-    /** The required mDNS service types */
-    private static final Set<String> PRINTER_SERVICE_TYPE = Set.of(
-            PRIVET_SERVICE); // Not checking _printer_._sub
-
-    /** All possible connection states */
-    private static final Set<String> POSSIBLE_CONNECTION_STATES = Set.of(
-            "online",
-            "offline",
-            "connecting",
-            "not-configured");
-
-    private static final byte SUPPORTED_TXTVERS = '1';
-
-    /** The mDNS filtered discovery */
-    private final MDNSFilteredDiscovery mMDNSFilteredDiscovery;
-
-    /**
-     * Create a plugin detecting Google Cloud Print printers.
-     *
-     * @param context The context the plugin runs in
-     */
-    public CloudPrintPlugin(@NonNull Context context) {
-        mMDNSFilteredDiscovery = new MDNSFilteredDiscovery(context, PRINTER_SERVICE_TYPE,
-                nsdServiceInfo -> {
-                    // The attributes are case insensitive. For faster searching create a clone of
-                    // the map with the attribute-keys all in lower case.
-                    ArrayMap<String, byte[]> caseInsensitiveAttributes =
-                            new ArrayMap<>(nsdServiceInfo.getAttributes().size());
-                    for (Map.Entry<String, byte[]> entry : nsdServiceInfo.getAttributes()
-                            .entrySet()) {
-                        caseInsensitiveAttributes.put(entry.getKey().toLowerCase(),
-                                entry.getValue());
-                    }
-
-                    if (DEBUG) {
-                        Log.i(LOG_TAG, nsdServiceInfo.getServiceName() + ":");
-                        Log.i(LOG_TAG, "type:  " + nsdServiceInfo.getServiceType());
-                        Log.i(LOG_TAG, "host:  " + nsdServiceInfo.getHost());
-                        for (Map.Entry<String, byte[]> entry : caseInsensitiveAttributes.entrySet()) {
-                            if (entry.getValue() == null) {
-                                Log.i(LOG_TAG, entry.getKey() + "= null");
-                            } else {
-                                Log.i(LOG_TAG, entry.getKey() + "=" + new String(entry.getValue(),
-                                        StandardCharsets.UTF_8));
-                            }
-                        }
-                    }
-
-                    byte[] txtvers = caseInsensitiveAttributes.get(ATTRIBUTE_TXTVERS);
-                    if (txtvers == null || txtvers.length != 1 || txtvers[0] != SUPPORTED_TXTVERS) {
-                        // The spec requires this to be the first attribute, but at this time we
-                        // lost the order of the attributes
-                        return false;
-                    }
-
-                    if (caseInsensitiveAttributes.get(ATTRIBUTE_TY) == null) {
-                        return false;
-                    }
-
-                    byte[] url = caseInsensitiveAttributes.get(ATTRIBUTE_URL);
-                    if (url == null || url.length == 0) {
-                        return false;
-                    }
-
-                    byte[] type = caseInsensitiveAttributes.get(ATTRIBUTE_TYPE);
-                    if (type == null || !TYPE.equals(
-                            new String(type, StandardCharsets.UTF_8).toLowerCase())) {
-                        return false;
-                    }
-
-                    if (caseInsensitiveAttributes.get(ATTRIBUTE_ID) == null) {
-                        return false;
-                    }
-
-                    byte[] cs = caseInsensitiveAttributes.get(ATTRIBUTE_CS);
-                    if (cs == null || !POSSIBLE_CONNECTION_STATES.contains(
-                            new String(cs, StandardCharsets.UTF_8).toLowerCase())) {
-                        return false;
-                    }
-
-                    InetAddress address = nsdServiceInfo.getHost();
-                    if (!(address instanceof Inet4Address)) {
-                        // Not checking for link local address
-                        return false;
-                    }
-
-                    return true;
-                });
-    }
-
-    @Override
-    @NonNull public CharSequence getPackageName() {
-        return "com.google.android.apps.cloudprint";
-    }
-
-    @Override
-    public void start(@NonNull PrinterDiscoveryCallback callback) throws Exception {
-        mMDNSFilteredDiscovery.start(callback);
-    }
-
-    @Override
-    @StringRes public int getName() {
-        return R.string.plugin_vendor_google_cloud_print;
-    }
-
-    @Override
-    public void stop() throws Exception {
-        mMDNSFilteredDiscovery.stop();
-    }
-}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index e185367..761bb79 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -27,7 +27,7 @@
 import com.android.settingslib.spa.gallery.dialog.DialogMainPageProvider
 import com.android.settingslib.spa.gallery.dialog.NavDialogProvider
 import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider
-import com.android.settingslib.spa.gallery.editor.SettingsExposedDropdownMenuBoxPageProvider
+import com.android.settingslib.spa.gallery.editor.SettingsDropdownBoxPageProvider
 import com.android.settingslib.spa.gallery.editor.SettingsDropdownCheckBoxProvider
 import com.android.settingslib.spa.gallery.home.HomePageProvider
 import com.android.settingslib.spa.gallery.itemList.ItemListPageProvider
@@ -99,7 +99,7 @@
                 OperateListPageProvider,
                 EditorMainPageProvider,
                 SettingsOutlinedTextFieldPageProvider,
-                SettingsExposedDropdownMenuBoxPageProvider,
+                SettingsDropdownBoxPageProvider,
                 SettingsDropdownCheckBoxProvider,
                 SettingsTextFieldPasswordPageProvider,
                 SearchScaffoldPageProvider,
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt
index 9f2158a..c511542 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt
@@ -35,7 +35,7 @@
         return listOf(
             SettingsOutlinedTextFieldPageProvider.buildInjectEntry().setLink(fromPage = owner)
                 .build(),
-            SettingsExposedDropdownMenuBoxPageProvider.buildInjectEntry().setLink(fromPage = owner)
+            SettingsDropdownBoxPageProvider.buildInjectEntry().setLink(fromPage = owner)
                 .build(),
             SettingsDropdownCheckBoxProvider.buildInjectEntry().setLink(fromPage = owner)
                 .build(),
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuBoxPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsDropdownBoxPageProvider.kt
similarity index 63%
rename from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuBoxPageProvider.kt
rename to packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsDropdownBoxPageProvider.kt
index 5ffbe8ba..2ebb5f5 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuBoxPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsDropdownBoxPageProvider.kt
@@ -28,16 +28,15 @@
 import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.widget.editor.SettingsExposedDropdownMenuBox
+import com.android.settingslib.spa.widget.editor.SettingsDropdownBox
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
 import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 
-private const val TITLE = "Sample SettingsExposedDropdownMenuBox"
+private const val TITLE = "Sample SettingsDropdownBox"
 
-object SettingsExposedDropdownMenuBoxPageProvider : SettingsPageProvider {
-    override val name = "SettingsExposedDropdownMenuBox"
-    private const val exposedDropdownMenuBoxLabel = "ExposedDropdownMenuBoxLabel"
+object SettingsDropdownBoxPageProvider : SettingsPageProvider {
+    override val name = "SettingsDropdownBox"
 
     override fun getTitle(arguments: Bundle?): String {
         return TITLE
@@ -45,18 +44,44 @@
 
     @Composable
     override fun Page(arguments: Bundle?) {
-        var selectedItem by remember { mutableIntStateOf(-1) }
-        val options = listOf("item1", "item2", "item3")
         RegularScaffold(title = TITLE) {
-            SettingsExposedDropdownMenuBox(
-                label = exposedDropdownMenuBoxLabel,
-                options = options,
-                selectedOptionIndex = selectedItem,
-                enabled = true,
-                onselectedOptionTextChange = { selectedItem = it })
+            Regular()
+            NotEnabled()
+            Empty()
         }
     }
 
+    @Composable
+    private fun Regular() {
+        var selectedItem by remember { mutableIntStateOf(-1) }
+        SettingsDropdownBox(
+            label = "SettingsDropdownBox",
+            options = listOf("item1", "item2", "item3"),
+            selectedOptionIndex = selectedItem,
+        ) { selectedItem = it }
+    }
+
+    @Composable
+    private fun NotEnabled() {
+        var selectedItem by remember { mutableIntStateOf(0) }
+        SettingsDropdownBox(
+            label = "Not enabled",
+            options = listOf("item1", "item2", "item3"),
+            enabled = false,
+            selectedOptionIndex = selectedItem,
+        ) { selectedItem = it }
+    }
+
+    @Composable
+    private fun Empty() {
+        var selectedItem by remember { mutableIntStateOf(-1) }
+        SettingsDropdownBox(
+            label = "Empty",
+            options = emptyList(),
+            selectedOptionIndex = selectedItem,
+        ) { selectedItem = it }
+    }
+
     fun buildInjectEntry(): SettingsEntryBuilder {
         return SettingsEntryBuilder.createInject(owner = createSettingsPage())
             .setUiLayoutFn {
@@ -70,8 +95,8 @@
 
 @Preview(showBackground = true)
 @Composable
-private fun SettingsExposedDropdownMenuBoxPagePreview() {
+private fun SettingsDropdownBoxPagePreview() {
     SettingsTheme {
-        SettingsExposedDropdownMenuBoxPageProvider.Page(null)
+        SettingsDropdownBoxPageProvider.Page(null)
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/DropdownTextBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/DropdownTextBox.kt
new file mode 100644
index 0000000..679c562
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/DropdownTextBox.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.settingslib.spa.widget.editor
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+
+internal interface DropdownTextBoxScope {
+    fun dismiss()
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun DropdownTextBox(
+    label: String,
+    text: String,
+    enabled: Boolean = true,
+    errorMessage: String? = null,
+    content: @Composable DropdownTextBoxScope.() -> Unit,
+) {
+    var expanded by remember { mutableStateOf(false) }
+    val scope = remember {
+        object : DropdownTextBoxScope {
+            override fun dismiss() {
+                expanded = false
+            }
+        }
+    }
+    ExposedDropdownMenuBox(
+        expanded = expanded,
+        onExpandedChange = { expanded = enabled && it },
+        modifier = Modifier
+            .padding(SettingsDimension.menuFieldPadding)
+            .width(Width),
+    ) {
+        OutlinedTextField(
+            // The `menuAnchor` modifier must be passed to the text field for correctness.
+            modifier = Modifier
+                .menuAnchor()
+                .fillMaxWidth(),
+            value = text,
+            onValueChange = { },
+            label = { Text(text = label) },
+            trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
+            singleLine = true,
+            readOnly = true,
+            enabled = enabled,
+            isError = errorMessage != null,
+            supportingText = errorMessage?.let { { Text(text = it) } },
+        )
+        ExposedDropdownMenu(
+            expanded = expanded,
+            modifier = Modifier.width(Width),
+            onDismissRequest = { expanded = false },
+        ) { scope.content() }
+    }
+}
+
+private val Width = 310.dp
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBox.kt
new file mode 100644
index 0000000..ff141c2
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBox.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.settingslib.spa.widget.editor
+
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+fun SettingsDropdownBox(
+    label: String,
+    options: List<String>,
+    selectedOptionIndex: Int,
+    enabled: Boolean = true,
+    onSelectedOptionChange: (Int) -> Unit,
+) {
+    DropdownTextBox(
+        label = label,
+        text = options.getOrElse(selectedOptionIndex) { "" },
+        enabled = enabled && options.isNotEmpty(),
+    ) {
+        options.forEachIndexed { index, option ->
+            DropdownMenuItem(
+                text = { Text(option) },
+                onClick = {
+                    dismiss()
+                    onSelectedOptionChange(index)
+                },
+                contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
+            )
+        }
+    }
+}
+
+@Preview
+@Composable
+private fun SettingsDropdownBoxPreview() {
+    val item1 = "item1"
+    val item2 = "item2"
+    val item3 = "item3"
+    val options = listOf(item1, item2, item3)
+    SettingsTheme {
+        SettingsDropdownBox(
+            label = "ExposedDropdownMenuBoxLabel",
+            options = options,
+            selectedOptionIndex = 0,
+            enabled = true,
+        ) {}
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt
index 57963e6..0e7e499 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt
@@ -19,28 +19,15 @@
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
 import androidx.compose.material3.Checkbox
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExposedDropdownMenuBox
-import androidx.compose.material3.ExposedDropdownMenuDefaults
-import androidx.compose.material3.OutlinedTextField
 import androidx.compose.material3.Text
 import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsOpacity.alphaForEnabled
 import com.android.settingslib.spa.framework.theme.SettingsTheme
@@ -68,7 +55,6 @@
     }
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun SettingsDropdownCheckBox(
     label: String,
@@ -78,43 +64,18 @@
     errorMessage: String? = null,
     onSelectedStateChange: () -> Unit = {},
 ) {
-    var dropDownWidth by remember { mutableIntStateOf(0) }
-    var expanded by remember { mutableStateOf(false) }
-    val changeable = enabled && options.changeable
-    ExposedDropdownMenuBox(
-        expanded = expanded,
-        onExpandedChange = { expanded = changeable && it },
-        modifier = Modifier
-            .width(350.dp)
-            .padding(SettingsDimension.textFieldPadding)
-            .onSizeChanged { dropDownWidth = it.width },
+    DropdownTextBox(
+        label = label,
+        text = getDisplayText(options) ?: emptyText,
+        enabled = enabled && options.changeable,
+        errorMessage = errorMessage,
     ) {
-        OutlinedTextField(
-            // The `menuAnchor` modifier must be passed to the text field for correctness.
-            modifier = Modifier
-                .menuAnchor()
-                .fillMaxWidth(),
-            value = getDisplayText(options) ?: emptyText,
-            onValueChange = {},
-            label = { Text(text = label) },
-            trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
-            readOnly = true,
-            enabled = changeable,
-            isError = errorMessage != null,
-            supportingText = errorMessage?.let { { Text(text = it) } },
-        )
-        ExposedDropdownMenu(
-            expanded = expanded,
-            modifier = Modifier.width(with(LocalDensity.current) { dropDownWidth.toDp() }),
-            onDismissRequest = { expanded = false },
-        ) {
-            for (option in options) {
-                CheckboxItem(option) {
-                    option.onClick()
-                    if (option.changeable) {
-                        checkboxItemOnClick(options, option)
-                        onSelectedStateChange()
-                    }
+        for (option in options) {
+            CheckboxItem(option) {
+                option.onClick()
+                if (option.changeable) {
+                    checkboxItemOnClick(options, option)
+                    onSelectedStateChange()
                 }
             }
         }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt
deleted file mode 100644
index f6692a3..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.widget.editor
-
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExposedDropdownMenuBox
-import androidx.compose.material3.ExposedDropdownMenuDefaults
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import com.android.settingslib.spa.framework.theme.SettingsDimension
-import com.android.settingslib.spa.framework.theme.SettingsTheme
-
-@Composable
-@OptIn(ExperimentalMaterial3Api::class)
-fun SettingsExposedDropdownMenuBox(
-    label: String,
-    options: List<String>,
-    selectedOptionIndex: Int,
-    enabled: Boolean,
-    onselectedOptionTextChange: (Int) -> Unit,
-) {
-    var expanded by remember { mutableStateOf(false) }
-    ExposedDropdownMenuBox(
-        expanded = expanded,
-        onExpandedChange = { expanded = it },
-        modifier = Modifier
-            .width(350.dp)
-            .padding(SettingsDimension.menuFieldPadding),
-    ) {
-        OutlinedTextField(
-            // The `menuAnchor` modifier must be passed to the text field for correctness.
-            modifier = Modifier
-                .menuAnchor()
-                .fillMaxWidth(),
-            value = options.getOrElse(selectedOptionIndex) { "" },
-            onValueChange = { },
-            label = { Text(text = label) },
-            trailingIcon = {
-                ExposedDropdownMenuDefaults.TrailingIcon(
-                    expanded = expanded
-                )
-            },
-            singleLine = true,
-            readOnly = true,
-            enabled = enabled
-        )
-        if (options.isNotEmpty()) {
-            ExposedDropdownMenu(
-                expanded = expanded,
-                modifier = Modifier
-                    .fillMaxWidth(),
-                onDismissRequest = { expanded = false },
-            ) {
-                options.forEach { option ->
-                    DropdownMenuItem(
-                        text = { Text(option) },
-                        onClick = {
-                            onselectedOptionTextChange(options.indexOf(option))
-                            expanded = false
-                        },
-                        contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
-                    )
-                }
-            }
-        }
-    }
-}
-
-@Preview
-@Composable
-private fun SettingsExposedDropdownMenuBoxsPreview() {
-    val item1 = "item1"
-    val item2 = "item2"
-    val item3 = "item3"
-    val options = listOf(item1, item2, item3)
-    SettingsTheme {
-        SettingsExposedDropdownMenuBox(
-            label = "ExposedDropdownMenuBoxLabel",
-            options = options,
-            selectedOptionIndex = 0,
-            enabled = true,
-            onselectedOptionTextChange = {})
-    }
-}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBoxTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBoxTest.kt
new file mode 100644
index 0000000..c347424
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBoxTest.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.settingslib.spa.widget.editor
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SettingsDropdownBoxTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun dropdownMenuBox_displayed() {
+        composeTestRule.setContent {
+            var selectedItem by remember { mutableStateOf(0) }
+            SettingsDropdownBox(
+                label = LABEL,
+                options = options,
+                selectedOptionIndex = selectedItem,
+            ) { selectedItem = it }
+        }
+
+        composeTestRule.onNodeWithText(LABEL).assertIsDisplayed()
+    }
+
+    @Test
+    fun dropdownMenuBox_enabled_expanded() {
+        composeTestRule.setContent {
+            var selectedItem by remember { mutableIntStateOf(0) }
+            SettingsDropdownBox(
+                label = LABEL,
+                options = options,
+                selectedOptionIndex = selectedItem
+            ) { selectedItem = it }
+        }
+        composeTestRule.onNodeWithText(ITEM2).assertDoesNotExist()
+
+        composeTestRule.onNodeWithText(LABEL).performClick()
+
+        composeTestRule.onNodeWithText(ITEM2).assertIsDisplayed()
+    }
+
+    @Test
+    fun dropdownMenuBox_notEnabled_notExpanded() {
+        composeTestRule.setContent {
+            var selectedItem by remember { mutableIntStateOf(0) }
+            SettingsDropdownBox(
+                label = LABEL,
+                options = options,
+                enabled = false,
+                selectedOptionIndex = selectedItem
+            ) { selectedItem = it }
+        }
+        composeTestRule.onNodeWithText(ITEM2).assertDoesNotExist()
+
+        composeTestRule.onNodeWithText(LABEL).performClick()
+
+        composeTestRule.onNodeWithText(ITEM2).assertDoesNotExist()
+    }
+
+    @Test
+    fun dropdownMenuBox_valueChanged() {
+        composeTestRule.setContent {
+            var selectedItem by remember { mutableIntStateOf(0) }
+            SettingsDropdownBox(
+                label = LABEL,
+                options = options,
+                selectedOptionIndex = selectedItem
+            ) { selectedItem = it }
+        }
+        composeTestRule.onNodeWithText(ITEM2).assertDoesNotExist()
+
+        composeTestRule.onNodeWithText(LABEL).performClick()
+        composeTestRule.onNodeWithText(ITEM2).performClick()
+
+        composeTestRule.onNodeWithText(ITEM2).assertIsDisplayed()
+    }
+    private companion object {
+        const val LABEL = "Label"
+        const val ITEM2 = "item2"
+        val options = listOf("item1", ITEM2, "item3")
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBoxTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBoxTest.kt
deleted file mode 100644
index bc67e4c..0000000
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBoxTest.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.widget.editor
-
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performClick
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class SettingsExposedDropdownMenuBoxTest {
-    @get:Rule
-    val composeTestRule = createComposeRule()
-    private val options = listOf("item1", "item2", "item3")
-    private val item2 = "item2"
-    private val exposedDropdownMenuBoxLabel = "ExposedDropdownMenuBoxLabel"
-
-    @Test
-    fun exposedDropdownMenuBoxs_displayed() {
-        composeTestRule.setContent {
-            var selectedItem by remember { mutableStateOf(0) }
-            SettingsExposedDropdownMenuBox(
-                label = exposedDropdownMenuBoxLabel,
-                options = options,
-                selectedOptionIndex = selectedItem,
-                enabled = true,
-                onselectedOptionTextChange = { selectedItem = it })
-        }
-        composeTestRule.onNodeWithText(exposedDropdownMenuBoxLabel, substring = true)
-            .assertIsDisplayed()
-    }
-
-    @Test
-    fun exposedDropdownMenuBoxs_expanded() {
-        composeTestRule.setContent {
-            var selectedItem by remember { mutableIntStateOf(0) }
-            SettingsExposedDropdownMenuBox(
-                label = exposedDropdownMenuBoxLabel,
-                options = options,
-                selectedOptionIndex = selectedItem,
-                enabled = true,
-                onselectedOptionTextChange = { selectedItem = it })
-        }
-        composeTestRule.onNodeWithText(item2, substring = true)
-            .assertDoesNotExist()
-        composeTestRule.onNodeWithText(exposedDropdownMenuBoxLabel, substring = true)
-            .performClick()
-        composeTestRule.onNodeWithText(item2, substring = true)
-            .assertIsDisplayed()
-    }
-
-    @Test
-    fun exposedDropdownMenuBoxs_valueChanged() {
-        composeTestRule.setContent {
-            var selectedItem by remember { mutableIntStateOf(0) }
-            SettingsExposedDropdownMenuBox(
-                label = exposedDropdownMenuBoxLabel,
-                options = options,
-                selectedOptionIndex = selectedItem,
-                enabled = true,
-                onselectedOptionTextChange = { selectedItem = it })
-        }
-        composeTestRule.onNodeWithText(item2, substring = true)
-            .assertDoesNotExist()
-        composeTestRule.onNodeWithText(exposedDropdownMenuBoxLabel, substring = true)
-            .performClick()
-        composeTestRule.onNodeWithText(item2, substring = true)
-            .performClick()
-        composeTestRule.onNodeWithText(item2, substring = true)
-            .assertIsDisplayed()
-    }
-}
\ No newline at end of file
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 cddd4fa..622a4f0 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
@@ -73,6 +73,7 @@
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
@@ -85,7 +86,10 @@
 import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
@@ -110,6 +114,7 @@
 import com.android.systemui.res.R
 import kotlinx.coroutines.launch
 
+@OptIn(ExperimentalComposeUiApi::class)
 @Composable
 fun CommunalHub(
     modifier: Modifier = Modifier,
@@ -140,6 +145,8 @@
     Box(
         modifier =
             modifier
+                .semantics { testTagsAsResourceId = true }
+                .testTag(COMMUNAL_HUB_TEST_TAG)
                 .fillMaxSize()
                 .pointerInput(gridState, contentOffset, contentListState) {
                     // If not in edit mode, don't allow selecting items.
@@ -872,3 +879,6 @@
         )
     val IconSize = 48.dp
 }
+
+/** The resource id of communal hub accessible from UiAutomator. */
+private const val COMMUNAL_HUB_TEST_TAG = "communal_hub"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index ed2cbb8..c7d43fc 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -55,7 +55,7 @@
     notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
     ambientState: AmbientState,
     notificationStackSizeCalculator: NotificationStackSizeCalculator,
-    @Main private val mainDispatcher: CoroutineDispatcher,
+    @Main private val mainImmediateDispatcher: CoroutineDispatcher,
 ) {
 
     init {
@@ -80,7 +80,7 @@
             sceneContainerFlags,
             controller,
             notificationStackSizeCalculator,
-            mainDispatcher,
+            mainImmediateDispatcher = mainImmediateDispatcher,
         )
 
         if (sceneContainerFlags.flexiNotifsEnabled()) {
@@ -90,6 +90,7 @@
                 notificationStackAppearanceViewModel,
                 ambientState,
                 controller,
+                mainImmediateDispatcher = mainImmediateDispatcher,
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
index d0f57c7..02c889d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
@@ -111,7 +111,7 @@
                 sceneContainerFlags,
                 controller,
                 notificationStackSizeCalculator,
-                mainDispatcher,
+                mainImmediateDispatcher = mainDispatcher,
             )
         )
 
@@ -123,6 +123,7 @@
                     notificationStackAppearanceViewModel,
                     ambientState,
                     controller,
+                    mainImmediateDispatcher = mainDispatcher,
                 )
             )
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
index a157785..76495cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
@@ -20,12 +20,14 @@
 import android.util.TypedValue
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.notification.stack.AmbientState
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
 import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.launch
 
@@ -40,8 +42,9 @@
         viewModel: NotificationStackAppearanceViewModel,
         ambientState: AmbientState,
         controller: NotificationStackScrollLayoutController,
+        @Main mainImmediateDispatcher: CoroutineDispatcher,
     ): DisposableHandle {
-        return view.repeatWhenAttached {
+        return view.repeatWhenAttached(mainImmediateDispatcher) {
             repeatOnLifecycle(Lifecycle.State.CREATED) {
                 launch {
                     viewModel.stackBounds.collect { bounds ->
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 373d209..d2e0386 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -801,8 +801,9 @@
         Log.d(TAG, "teardown: entered");
         setOrientation(mOriginalOrientation);
         Log.d(TAG, "teardown: after setOrientation");
-        mAnimatorTestRule.advanceTimeBy(mLongestHideShowAnimationDuration);
-        Log.d(TAG, "teardown: after advanceTimeBy");
+        // Unclear why we used to do this, and it seems to be a source of flakes
+        // mAnimatorTestRule.advanceTimeBy(mLongestHideShowAnimationDuration);
+        Log.d(TAG, "teardown: skipped advanceTimeBy");
         mTestableLooper.moveTimeForward(mLongestHideShowAnimationDuration);
         Log.d(TAG, "teardown: after moveTimeForward");
         mTestableLooper.processAllMessages();
diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
index d8a94d8..e0fe88a 100644
--- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
+++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
@@ -1277,6 +1277,20 @@
         }
 
         @Override
+        public void onCaptureFailed(int captureSequenceId, int reason) {
+            if (Flags.concertMode()) {
+                if (mCaptureCallback != null) {
+                    try {
+                        mCaptureCallback.onCaptureProcessFailed(captureSequenceId, reason);
+                    } catch (RemoteException e) {
+                        Log.e(TAG, "Failed to notify capture failure due to remote " +
+                                "exception!");
+                    }
+                }
+            }
+        }
+
+        @Override
         public void onCaptureSequenceCompleted(int captureSequenceId) {
             if (mCaptureCallback != null) {
                 try {
diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index 4a4c290..eb3c55c 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -255,6 +255,7 @@
 android.view.Display$HdrCapabilities
 android.view.Display$Mode
 android.view.DisplayInfo
+android.view.inputmethod.InputBinding
 
 android.hardware.SerialManager
 android.hardware.SerialManagerInternal
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index d4f049e..a754ba5 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -73,6 +73,16 @@
 }
 
 flag {
+    name: "handle_multi_device_input"
+    namespace: "accessibility"
+    description: "Select a single active device when a multi-device stream is received by AccessibilityInputFilter"
+    bug: "310014874"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "pinch_zoom_zero_min_span"
     namespace: "accessibility"
     description: "Whether to set min span of ScaleGestureDetector to zero."
@@ -87,6 +97,16 @@
 }
 
 flag {
+    name: "reset_hover_event_timer_on_action_up"
+    namespace: "accessibility"
+    description: "Reset the timer for sending hover events on receiving ACTION_UP to guarantee the correct amount of time is available between taps."
+    bug: "326260351"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "scan_packages_without_lock"
     namespace: "accessibility"
     description: "Scans packages for accessibility service/activity info without holding the A11yMS lock"
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index abcd8e2..16119d11 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -25,6 +25,7 @@
 import android.content.Context;
 import android.graphics.Region;
 import android.os.PowerManager;
+import android.os.SystemClock;
 import android.provider.Settings;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -35,6 +36,8 @@
 import android.view.InputFilter;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
 import android.view.accessibility.AccessibilityEvent;
 
 import com.android.server.LocalServices;
@@ -203,6 +206,62 @@
 
     private EventStreamState mKeyboardStreamState;
 
+    /**
+     * The last MotionEvent emitted from the input device that's currently active. This is used to
+     * keep track of which input device is currently active, and also to generate the cancel event
+     * if a new device becomes active.
+     */
+    private MotionEvent mLastActiveDeviceMotionEvent = null;
+
+    private static MotionEvent cancelMotion(MotionEvent event) {
+        if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
+                || event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT
+                || event.getActionMasked() == MotionEvent.ACTION_UP) {
+            throw new IllegalArgumentException("Can't cancel " + event);
+        }
+        final int action;
+        if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER
+                || event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE) {
+            action = MotionEvent.ACTION_HOVER_EXIT;
+        } else {
+            action = MotionEvent.ACTION_CANCEL;
+        }
+
+        final int pointerCount;
+        if (event.getActionMasked() == MotionEvent.ACTION_POINTER_UP) {
+            pointerCount = event.getPointerCount() - 1;
+        } else {
+            pointerCount = event.getPointerCount();
+        }
+        final PointerProperties[] properties = new PointerProperties[pointerCount];
+        final PointerCoords[] coords = new PointerCoords[pointerCount];
+        int newPointerIndex = 0;
+        for (int i = 0; i < event.getPointerCount(); i++) {
+            if (event.getActionMasked() == MotionEvent.ACTION_POINTER_UP) {
+                if (event.getActionIndex() == i) {
+                    // Skip the pointer that's going away
+                    continue;
+                }
+            }
+            final PointerCoords c = new PointerCoords();
+            c.x = event.getX(i);
+            c.y = event.getY(i);
+            coords[newPointerIndex] = c;
+            final PointerProperties p = new PointerProperties();
+            p.id = event.getPointerId(i);
+            p.toolType = event.getToolType(i);
+            properties[newPointerIndex] = p;
+            newPointerIndex++;
+        }
+
+        return MotionEvent.obtain(event.getDownTime(), SystemClock.uptimeMillis(), action,
+                pointerCount, properties, coords,
+                event.getMetaState(), event.getButtonState(),
+                event.getXPrecision(), event.getYPrecision(), event.getDeviceId(),
+                event.getEdgeFlags(), event.getSource(), event.getDisplayId(), event.getFlags(),
+                event.getClassification());
+    }
+
     AccessibilityInputFilter(Context context, AccessibilityManagerService service) {
         this(context, service, new SparseArray<>(0));
     }
@@ -260,6 +319,17 @@
                     AccessibilityTrace.FLAGS_INPUT_FILTER,
                     "event=" + event + ";policyFlags=" + policyFlags);
         }
+        if (Flags.handleMultiDeviceInput()) {
+            if (!shouldProcessMultiDeviceEvent(event, policyFlags)) {
+                // We are only allowing a single device to be active at a time.
+                return;
+            }
+        }
+
+        onInputEventInternal(event, policyFlags);
+    }
+
+    private void onInputEventInternal(InputEvent event, int policyFlags) {
         if (mEventHandler.size() == 0) {
             if (DEBUG) Slog.d(TAG, "No mEventHandler for event " + event);
             super.onInputEvent(event, policyFlags);
@@ -353,6 +423,63 @@
         }
     }
 
+    boolean shouldProcessMultiDeviceEvent(InputEvent event, int policyFlags) {
+        if (event instanceof MotionEvent motion) {
+            // Only allow 1 device to be sending motion events at a time
+            // If the event is from an active device, let it through.
+            // If the event is not from an active device, only let it through if it starts a new
+            // gesture like ACTION_DOWN or ACTION_HOVER_ENTER
+            final boolean eventIsFromCurrentDevice = mLastActiveDeviceMotionEvent != null
+                    && mLastActiveDeviceMotionEvent.getDeviceId() == motion.getDeviceId();
+            final int actionMasked = motion.getActionMasked();
+            switch (actionMasked) {
+                case MotionEvent.ACTION_DOWN:
+                case MotionEvent.ACTION_HOVER_ENTER:
+                case MotionEvent.ACTION_HOVER_MOVE: {
+                    if (mLastActiveDeviceMotionEvent != null
+                            && mLastActiveDeviceMotionEvent.getDeviceId() != motion.getDeviceId()) {
+                        // This is a new gesture from a new device. Cancel the existing state
+                        // and let this through
+                        MotionEvent canceled = cancelMotion(mLastActiveDeviceMotionEvent);
+                        onInputEventInternal(canceled, policyFlags);
+                    }
+                    mLastActiveDeviceMotionEvent = MotionEvent.obtain(motion);
+                    return true;
+                }
+                case MotionEvent.ACTION_MOVE:
+                case MotionEvent.ACTION_POINTER_DOWN:
+                case MotionEvent.ACTION_POINTER_UP: {
+                    if (eventIsFromCurrentDevice) {
+                        mLastActiveDeviceMotionEvent = MotionEvent.obtain(motion);
+                        return true;
+                    } else {
+                        return false;
+                    }
+                }
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL:
+                case MotionEvent.ACTION_HOVER_EXIT: {
+                    if (eventIsFromCurrentDevice) {
+                        // This is the last event of the gesture from this device.
+                        mLastActiveDeviceMotionEvent = null;
+                        return true;
+                    } else {
+                        // Event is from another device
+                        return false;
+                    }
+                }
+                default: {
+                    if (mLastActiveDeviceMotionEvent != null
+                            && event.getDeviceId() != mLastActiveDeviceMotionEvent.getDeviceId()) {
+                        // This is an event from another device, ignore it.
+                        return false;
+                    }
+                }
+            }
+        }
+        return true;
+    }
+
     private void processMotionEvent(EventStreamState state, MotionEvent event, int policyFlags) {
         if (!state.shouldProcessScroll() && event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
             super.onInputEvent(event, policyFlags);
diff --git a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
index 9b27dd3..40b6ff0 100644
--- a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
@@ -232,9 +232,63 @@
     }
 
     /** Returns true if this descriptor includes usages for the Braille display usage page 0x41. */
-    private static boolean isBrailleDisplay(byte[] descriptor) {
-        // TODO: b/316036493 - Check that descriptor includes 0x41 reports.
-        return true;
+    @VisibleForTesting
+    static boolean isBrailleDisplay(byte[] descriptor) {
+        boolean foundMatch = false;
+        for (int i = 0; i < descriptor.length; i++) {
+            // HID Spec "6.2.2.2 Short Items" defines that the report descriptor is a collection of
+            // items: each item is a collection of bytes where the first byte defines info about
+            // the type of item and the following 0, 1, 2, or 4 bytes are data bytes for that item.
+            // All items in the HID descriptor are expected to be Short Items.
+            final byte itemInfo = descriptor[i];
+            if (!isHidItemShort(itemInfo)) {
+                Slog.w(LOG_TAG, "Item " + itemInfo + " declares unsupported long type");
+                return false;
+            }
+            final int dataSize = getHidItemDataSize(itemInfo);
+            if (i + dataSize >= descriptor.length) {
+                Slog.w(LOG_TAG, "Item " + itemInfo + " specifies size past the remaining bytes");
+                return false;
+            }
+            // The item we're looking for (usage page declaration) should have size 1.
+            if (dataSize == 1) {
+                final byte itemData = descriptor[i + 1];
+                if (isHidItemBrailleDisplayUsagePage(itemInfo, itemData)) {
+                    foundMatch = true;
+                }
+            }
+            // Move to the next item by skipping past all data bytes in this item.
+            i += dataSize;
+        }
+        return foundMatch;
+    }
+
+    private static boolean isHidItemShort(byte itemInfo) {
+        // Info bits 7-4 describe the item type, and HID Spec "6.2.2.3 Long Items" says that long
+        // items always have type bits 1111. Otherwise, the item is a short item.
+        return (itemInfo & 0b1111_0000) != 0b1111_0000;
+    }
+
+    private static int getHidItemDataSize(byte itemInfo) {
+        // HID Spec "6.2.2.2 Short Items" says that info bits 0-1 specify the optional data size:
+        // 0, 1, 2, or 4 bytes.
+        return switch (itemInfo & 0b0000_0011) {
+            case 0b00 -> 0;
+            case 0b01 -> 1;
+            case 0b10 -> 2;
+            default -> 4;
+        };
+    }
+
+    private static boolean isHidItemBrailleDisplayUsagePage(byte itemInfo, byte itemData) {
+        // From HID Spec "6.2.2.7 Global Items"
+        final byte usagePageType = 0b0000_0100;
+        // From HID Usage Tables version 1.2.
+        final byte brailleDisplayUsagePage = 0x41;
+        // HID Spec "6.2.2.2 Short Items" says item info bits 2-7 describe the type and
+        // function of the item.
+        final byte itemType = (byte) (itemInfo & 0b1111_1100);
+        return itemType == usagePageType && itemData == brailleDisplayUsagePage;
     }
 
     /**
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java b/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java
index b6223c7..bf9202f1b 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java
@@ -106,11 +106,30 @@
                 return;
             }
         }
+        final long downTime;
         if (action == MotionEvent.ACTION_DOWN) {
-            event.setDownTime(event.getEventTime());
+            downTime = event.getEventTime();
         } else {
-            event.setDownTime(mState.getLastInjectedDownEventTime());
+            downTime = mState.getLastInjectedDownEventTime();
         }
+
+        // The only way to change device id of the motion event is by re-creating the whole thing
+        final PointerProperties[] properties = new PointerProperties[event.getPointerCount()];
+        final PointerCoords[] coords = new PointerCoords[event.getPointerCount()];
+        for (int i = 0; i < event.getPointerCount(); i++) {
+            final PointerCoords c = new PointerCoords();
+            event.getPointerCoords(i, c);
+            coords[i] = c;
+            final PointerProperties p = new PointerProperties();
+            event.getPointerProperties(i, p);
+            properties[i] = p;
+        }
+        event = MotionEvent.obtain(downTime, event.getEventTime(), event.getAction(),
+                event.getPointerCount(), properties, coords,
+                event.getMetaState(), event.getButtonState(),
+                event.getXPrecision(), event.getYPrecision(), rawEvent.getDeviceId(),
+                event.getEdgeFlags(), rawEvent.getSource(), event.getDisplayId(), event.getFlags(),
+                event.getClassification());
         // If the user is long pressing but the long pressing pointer
         // was not exactly over the accessibility focused item we need
         // to remap the location of that pointer so the user does not
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index 3086ce1..4fc65bf 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -852,6 +852,11 @@
         final int pointerIdBits = (1 << pointerId);
         if (mSendHoverEnterAndMoveDelayed.isPending()) {
             // If we have not delivered the enter schedule an exit.
+            if (Flags.resetHoverEventTimerOnActionUp()) {
+                // We cancel first to reset the time window so that the user has the full amount of
+                // time to do a multi tap.
+                mSendHoverEnterAndMoveDelayed.repost();
+            }
             mSendHoverExitDelayed.post(event, rawEvent, pointerIdBits, policyFlags);
         } else {
             // The user is touch exploring so we send events for end.
@@ -1554,6 +1559,14 @@
             }
         }
 
+        public void repost() {
+            // cancel without clearing
+            if (isPending()) {
+                mHandler.removeCallbacks(this);
+                mHandler.postDelayed(this, mDetermineUserIntentTimeout);
+            }
+        }
+
         private boolean isPending() {
             return mHandler.hasCallbacks(this);
         }
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java
index 84a59b4..7251ac4 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java
@@ -43,6 +43,9 @@
  * the given {@link Handler} thread if {@link IInputMethodClient} is not a proxy object. Be careful
  * about its call ordering characteristics.</p>
  */
+// TODO(b/322895594) Mark this class to be host side test compatible once enabling fw/services in
+//     Ravenwood (mark this class with @RavenwoodKeepWholeClass and #create with @RavenwoodReplace,
+//     so Ravenwood can properly swap create method during test execution).
 final class IInputMethodClientInvoker {
     private static final String TAG = InputMethodManagerService.TAG;
     private static final boolean DEBUG = InputMethodManagerService.DEBUG;
@@ -64,6 +67,16 @@
         return new IInputMethodClientInvoker(inputMethodClient, isProxy, isProxy ? null : handler);
     }
 
+    @AnyThread
+    @Nullable
+    static IInputMethodClientInvoker create$ravenwood(
+            @Nullable IInputMethodClient inputMethodClient, @NonNull Handler handler) {
+        if (inputMethodClient == null) {
+            return null;
+        }
+        return new IInputMethodClientInvoker(inputMethodClient, true, null);
+    }
+
     private IInputMethodClientInvoker(@NonNull IInputMethodClient target,
             boolean isProxy, @Nullable Handler handler) {
         mTarget = target;
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 9b347d5..bf06c2a 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -15430,17 +15430,18 @@
             mPerDisplayBatteryStats[i].screenStateAtLastEnergyMeasurement = screenState;
         }
 
-        final boolean compatibleConfig;
         if (supportedStandardBuckets != null) {
             final EnergyConsumerStats.Config config = new EnergyConsumerStats.Config(
                     supportedStandardBuckets, customBucketNames,
                     SUPPORTED_PER_PROCESS_STATE_STANDARD_ENERGY_BUCKETS,
                     getBatteryConsumerProcessStateNames());
 
-            if (mEnergyConsumerStatsConfig == null) {
-                compatibleConfig = true;
-            } else {
-                compatibleConfig = mEnergyConsumerStatsConfig.isCompatible(config);
+            if (mEnergyConsumerStatsConfig != null
+                    &&  !mEnergyConsumerStatsConfig.isCompatible(config)) {
+                // Supported power buckets changed since last boot.
+                // Existing data is no longer reliable.
+                resetAllStatsLocked(SystemClock.uptimeMillis(), SystemClock.elapsedRealtime(),
+                        RESET_REASON_ENERGY_CONSUMER_BUCKETS_CHANGE);
             }
 
             mEnergyConsumerStatsConfig = config;
@@ -15456,18 +15457,14 @@
                 mWifiPowerCalculator = new WifiPowerCalculator(mPowerProfile);
             }
         } else {
-            compatibleConfig = (mEnergyConsumerStatsConfig == null);
-            // EnergyConsumer no longer supported, wipe out the existing data.
+            if (mEnergyConsumerStatsConfig != null) {
+                // EnergyConsumer no longer supported, wipe out the existing data.
+                resetAllStatsLocked(SystemClock.uptimeMillis(), SystemClock.elapsedRealtime(),
+                        RESET_REASON_ENERGY_CONSUMER_BUCKETS_CHANGE);
+            }
             mEnergyConsumerStatsConfig = null;
             mGlobalEnergyConsumerStats = null;
         }
-
-        if (!compatibleConfig) {
-            // Supported power buckets changed since last boot.
-            // Existing data is no longer reliable.
-            resetAllStatsLocked(SystemClock.uptimeMillis(), SystemClock.elapsedRealtime(),
-                    RESET_REASON_ENERGY_CONSUMER_BUCKETS_CHANGE);
-        }
     }
 
     @GuardedBy("this")
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 10cbc66..85d81c4 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -104,6 +104,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
+import com.android.internal.util.ToBooleanFunction;
 import com.android.server.am.HostingRecord;
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.window.flags.Flags;
@@ -3025,11 +3026,17 @@
             return false;
         }
 
-        // boost if there's an Activity window that has FLAG_DIM_BEHIND flag.
-        return forAllWindows(
+        ToBooleanFunction<WindowState> getDimBehindWindow =
                 (w) -> (w.mAttrs.flags & FLAG_DIM_BEHIND) != 0 && w.mActivityRecord != null
                         && w.mActivityRecord.isEmbedded() && (w.mActivityRecord.isVisibleRequested()
-                        || w.mActivityRecord.isVisible()), true);
+                        || w.mActivityRecord.isVisible());
+        if (adjacentTf.forAllWindows(getDimBehindWindow, true)) {
+            // early return if the adjacent Tf has a dimming window.
+            return false;
+        }
+
+        // boost if there's an Activity window that has FLAG_DIM_BEHIND flag.
+        return forAllWindows(getDimBehindWindow, true);
     }
 
     @Override
diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
index ca23d62..fa63bc8 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
@@ -30,9 +30,7 @@
 import android.credentials.selection.Entry;
 import android.credentials.selection.GetCredentialProviderData;
 import android.credentials.selection.ProviderPendingIntentResponse;
-import android.os.Bundle;
 import android.os.ICancellationSignal;
-import android.service.autofill.Flags;
 import android.service.credentials.Action;
 import android.service.credentials.BeginGetCredentialOption;
 import android.service.credentials.BeginGetCredentialRequest;
@@ -44,7 +42,6 @@
 import android.service.credentials.RemoteEntry;
 import android.util.Pair;
 import android.util.Slog;
-import android.view.autofill.AutofillId;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -77,10 +74,6 @@
     @NonNull
     private final Map<String, CredentialOption> mBeginGetOptionToCredentialOptionMap;
 
-    @NonNull
-    private final Map<String, AutofillId> mCredentialEntryKeyToAutofilLIdMap;
-
-
     /** The complete request to be used in the second round. */
     private final android.credentials.GetCredentialRequest mCompleteRequest;
     private final CallingAppInfo mCallingAppInfo;
@@ -249,7 +242,6 @@
         mBeginGetOptionToCredentialOptionMap = new HashMap<>(beginGetOptionToCredentialOptionMap);
         mProviderResponseDataHandler = new ProviderResponseDataHandler(
                 ComponentName.unflattenFromString(hybridService));
-        mCredentialEntryKeyToAutofilLIdMap = new HashMap<>();
     }
 
     /** Called when the provider response has been updated by an external source. */
@@ -303,7 +295,7 @@
                     invokeCallbackOnInternalInvalidState();
                     return;
                 }
-                onCredentialEntrySelected(providerPendingIntentResponse, entryKey);
+                onCredentialEntrySelected(providerPendingIntentResponse);
                 break;
             case ACTION_ENTRY_KEY:
                 Action actionEntry = mProviderResponseDataHandler.getActionEntry(entryKey);
@@ -312,7 +304,7 @@
                     invokeCallbackOnInternalInvalidState();
                     return;
                 }
-                onActionEntrySelected(providerPendingIntentResponse, entryKey);
+                onActionEntrySelected(providerPendingIntentResponse);
                 break;
             case AUTHENTICATION_ACTION_ENTRY_KEY:
                 Action authenticationEntry = mProviderResponseDataHandler
@@ -342,7 +334,7 @@
                 break;
             case REMOTE_ENTRY_KEY:
                 if (mProviderResponseDataHandler.getRemoteEntry(entryKey) != null) {
-                    onRemoteEntrySelected(providerPendingIntentResponse, entryKey);
+                    onRemoteEntrySelected(providerPendingIntentResponse);
                 } else {
                     Slog.i(TAG, "Unexpected remote entry key");
                     invokeCallbackOnInternalInvalidState();
@@ -381,7 +373,7 @@
         return null;
     }
 
-    private Intent setUpFillInIntentWithFinalRequest(@NonNull String id, String entryKey) {
+    private Intent setUpFillInIntentWithFinalRequest(@NonNull String id) {
         // TODO: Determine if we should skip this entry if entry id is not set, or is set
         // but does not resolve to a valid option. For now, not skipping it because
         // it may be possible that the provider adds their own extras and expects to receive
@@ -392,13 +384,6 @@
             Slog.w(TAG, "Id from Credential Entry does not resolve to a valid option");
             return intent;
         }
-        AutofillId autofillId = credentialOption
-                .getCandidateQueryData()
-                .getParcelable(CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId.class);
-        if (autofillId != null && Flags.autofillCredmanIntegration()) {
-            intent.putExtra(CredentialProviderService.EXTRA_AUTOFILL_ID, autofillId);
-            mCredentialEntryKeyToAutofilLIdMap.put(entryKey, autofillId);
-        }
         return intent.putExtra(
                 CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST,
                 new GetCredentialRequest(
@@ -414,13 +399,12 @@
     }
 
     private void onRemoteEntrySelected(
-            ProviderPendingIntentResponse providerPendingIntentResponse, String entryKey) {
-        onCredentialEntrySelected(providerPendingIntentResponse, entryKey);
+            ProviderPendingIntentResponse providerPendingIntentResponse) {
+        onCredentialEntrySelected(providerPendingIntentResponse);
     }
 
     private void onCredentialEntrySelected(
-            ProviderPendingIntentResponse providerPendingIntentResponse,
-            String entryKey) {
+            ProviderPendingIntentResponse providerPendingIntentResponse) {
         if (providerPendingIntentResponse == null) {
             invokeCallbackOnInternalInvalidState();
             return;
@@ -437,18 +421,7 @@
         GetCredentialResponse getCredentialResponse = PendingIntentResultHandler
                 .extractGetCredentialResponse(
                         providerPendingIntentResponse.getResultData());
-        if (getCredentialResponse != null && getCredentialResponse.getCredential() != null) {
-            Bundle credentialData = getCredentialResponse.getCredential().getData();
-            AutofillId autofillId = mCredentialEntryKeyToAutofilLIdMap.get(entryKey);
-            if (Flags.autofillCredmanIntegration()
-                    && entryKey != null && autofillId != null && credentialData != null
-            ) {
-                Slog.d(TAG, "Adding autofillId to credential response: " + autofillId);
-                credentialData.putParcelable(
-                        CredentialProviderService.EXTRA_AUTOFILL_ID,
-                        mCredentialEntryKeyToAutofilLIdMap.get(entryKey)
-                );
-            }
+        if (getCredentialResponse != null) {
             mCallbacks.onFinalResponseReceived(mComponentName,
                     getCredentialResponse);
             return;
@@ -532,9 +505,9 @@
 
     /** Returns true if either an exception or a response is found. */
     private void onActionEntrySelected(ProviderPendingIntentResponse
-            providerPendingIntentResponse, String entryKey) {
+            providerPendingIntentResponse) {
         Slog.i(TAG, "onActionEntrySelected");
-        onCredentialEntrySelected(providerPendingIntentResponse, entryKey);
+        onCredentialEntrySelected(providerPendingIntentResponse);
     }
 
 
@@ -632,7 +605,7 @@
             Entry entry = new Entry(CREDENTIAL_ENTRY_KEY,
                     id, credentialEntry.getSlice(),
                     setUpFillInIntentWithFinalRequest(credentialEntry
-                            .getBeginGetCredentialOptionId(), id));
+                            .getBeginGetCredentialOptionId()));
             mUiCredentialEntries.put(id, new Pair<>(credentialEntry, entry));
             mCredentialEntryTypes.add(credentialEntry.getType());
         }
diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
index afd6dbd..b7af58c 100644
--- a/services/tests/InputMethodSystemServerTests/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/Android.bp
@@ -85,7 +85,6 @@
     srcs: [
         "src/com/android/server/inputmethod/**/ClientControllerTest.java",
     ],
-    sdk_version: "test_current",
     auto_gen_config: true,
 }
 
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
index dc9631a..9e3d9ec 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
@@ -32,10 +32,8 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
-import android.platform.test.annotations.IgnoreUnderRavenwood;
 import android.platform.test.ravenwood.RavenwoodRule;
 import android.view.Display;
-import android.view.inputmethod.InputBinding;
 
 import com.android.internal.inputmethod.IInputMethodClient;
 import com.android.internal.inputmethod.IRemoteInputConnection;
@@ -53,7 +51,7 @@
 public final class ClientControllerTest {
     private static final int ANY_DISPLAY_ID = Display.DEFAULT_DISPLAY;
     private static final int ANY_CALLER_UID = 1;
-    private static final int ANY_CALLER_PID = 1;
+    private static final int ANY_CALLER_PID = 2;
     private static final String SOME_PACKAGE_NAME = "some.package";
 
     @Rule
@@ -82,13 +80,16 @@
         mController = new ClientController(mMockPackageManagerInternal);
     }
 
-    @Test
-    // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for
-    //  inputmethod server classes.
-    @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class})
-    public void testAddClient_cannotAddTheSameClientTwice() {
-        var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
+    // TODO(b/322895594): No need to directly invoke create$ravenwood once b/322895594 is fixed.
+    private IInputMethodClientInvoker createInvoker(IInputMethodClient client, Handler handler) {
+        return RavenwoodRule.isOnRavenwood()
+                ? IInputMethodClientInvoker.create$ravenwood(client, handler) :
+                IInputMethodClientInvoker.create(client, handler);
+    }
 
+    @Test
+    public void testAddClient_cannotAddTheSameClientTwice() {
+        final var invoker = createInvoker(mClient, mHandler);
         synchronized (ImfLock.class) {
             mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID,
                     ANY_CALLER_PID);
@@ -101,18 +102,17 @@
                         }
                     });
             assertThat(thrown.getMessage()).isEqualTo(
-                    "uid=1/pid=1/displayId=0 is already registered");
+                    "uid=" + ANY_CALLER_UID + "/pid=" + ANY_CALLER_PID
+                            + "/displayId=0 is already registered");
         }
     }
 
     @Test
-    // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for
-    //  inputmethod server classes.
-    @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class})
     public void testAddClient() throws Exception {
+        final var invoker = createInvoker(mClient, mHandler);
         synchronized (ImfLock.class) {
-            var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
-            var added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID,
+            final var added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID,
+                    ANY_CALLER_UID,
                     ANY_CALLER_PID);
 
             verify(invoker.asBinder()).linkToDeath(any(IBinder.DeathRecipient.class), eq(0));
@@ -121,16 +121,12 @@
     }
 
     @Test
-    // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for
-    //  inputmethod server classes.
-    @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class})
     public void testRemoveClient() {
-        var callback = new TestClientControllerCallback();
+        final var invoker = createInvoker(mClient, mHandler);
+        final var callback = new TestClientControllerCallback();
         ClientState added;
         synchronized (ImfLock.class) {
             mController.addClientControllerCallback(callback);
-
-            var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
             added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID,
                     ANY_CALLER_PID);
             assertThat(mController.getClient(invoker.asBinder())).isSameInstanceAs(added);
@@ -138,21 +134,17 @@
         }
 
         // Test callback
-        var removed = callback.waitForRemovedClient(5, TimeUnit.SECONDS);
+        final var removed = callback.waitForRemovedClient(5, TimeUnit.SECONDS);
         assertThat(removed).isSameInstanceAs(added);
     }
 
     @Test
-    // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for
-    //  inputmethod server classes and updated to newer Mockito with static mock support (mock
-    //  InputMethodUtils#checkIfPackageBelongsToUid instead of PackageManagerInternal#isSameApp)
-    @IgnoreUnderRavenwood(blockedBy = {InputMethodUtils.class})
     public void testVerifyClientAndPackageMatch() {
+        final var invoker = createInvoker(mClient, mHandler);
         when(mMockPackageManagerInternal.isSameApp(eq(SOME_PACKAGE_NAME),  /* flags= */
                 anyLong(), eq(ANY_CALLER_UID), /* userId= */ anyInt())).thenReturn(true);
 
         synchronized (ImfLock.class) {
-            var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
             mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID,
                     ANY_CALLER_PID);
             assertThat(
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index a476155..9975221 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -16,6 +16,8 @@
 package com.android.server.alarm;
 
 import static android.Manifest.permission.SCHEDULE_EXACT_ALARM;
+import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN;
+import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN;
 import static android.app.AlarmManager.ELAPSED_REALTIME;
 import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
 import static android.app.AlarmManager.EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED;
@@ -42,6 +44,7 @@
 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
 import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
 import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED;
+import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.NULL_DEFAULT;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doCallRealMethod;
@@ -135,6 +138,7 @@
 import android.os.BatteryManager;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
@@ -145,9 +149,11 @@
 import android.os.ServiceManager;
 import android.os.SystemProperties;
 import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.platform.test.flag.util.FlagSetException;
 import android.provider.DeviceConfig;
-import android.provider.Settings;
 import android.text.format.DateFormat;
 import android.util.ArraySet;
 import android.util.Log;
@@ -215,6 +221,7 @@
     private AppStateTrackerImpl.Listener mListener;
     private AlarmManagerService.UninstallReceiver mPackageChangesReceiver;
     private AlarmManagerService.ChargingReceiver mChargingReceiver;
+    private ActivityManager.UidFrozenStateChangedCallback mUidFrozenStateCallback;
     private IAppOpsCallback mIAppOpsCallback;
     private IAlarmManager mBinder;
     @Mock
@@ -240,6 +247,8 @@
     @Mock
     private ActivityManagerInternal mActivityManagerInternal;
     @Mock
+    private ActivityManager mActivityManager;
+    @Mock
     private PackageManagerInternal mPackageManagerInternal;
     @Mock
     private AppStateTrackerImpl mAppStateTracker;
@@ -403,15 +412,31 @@
             .mockStatic(PermissionChecker.class)
             .mockStatic(PermissionManagerService.class)
             .mockStatic(ServiceManager.class)
-            .mockStatic(Settings.Global.class)
             .mockStatic(SystemProperties.class)
             .spyStatic(UserHandle.class)
             .afterSessionFinished(
                     () -> LocalServices.removeServiceForTest(AlarmManagerInternal.class))
             .build();
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(NULL_DEFAULT);
+
+    /**
+     * Have to do this to switch the {@link Flags} implementation to {@link FakeFeatureFlagsImpl}.
+     * All methods that need any flag enabled should use the
+     * {@link android.platform.test.annotations.EnableFlags} annotation, in which case disabling
+     * the flag will fail with an exception that we will swallow here.
+     */
+    private void disableFlagsNotSetByAnnotation() {
+        try {
+            mSetFlagsRule.disableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS);
+        } catch (FlagSetException fse) {
+            // Expected if the test about to be run requires this enabled.
+        }
+    }
+
     @Before
-    public final void setUp() {
+    public void setUp() {
         doReturn(mIActivityManager).when(ActivityManager::getService);
         doReturn(mDeviceIdleInternal).when(
                 () -> LocalServices.getService(DeviceIdleInternal.class));
@@ -469,6 +494,7 @@
 
         when(mMockContext.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mAppOpsManager);
         when(mMockContext.getSystemService(BatteryManager.class)).thenReturn(mBatteryManager);
+        when(mMockContext.getSystemService(ActivityManager.class)).thenReturn(mActivityManager);
 
         registerAppIds(new String[]{TEST_CALLING_PACKAGE},
                 new Integer[]{UserHandle.getAppId(TEST_CALLING_UID)});
@@ -479,7 +505,18 @@
         mService = new AlarmManagerService(mMockContext, mInjector);
         spyOn(mService);
 
+        disableFlagsNotSetByAnnotation();
+
         mService.onStart();
+
+        if (Flags.useFrozenStateToDropListenerAlarms()) {
+            final ArgumentCaptor<ActivityManager.UidFrozenStateChangedCallback> frozenCaptor =
+                    ArgumentCaptor.forClass(ActivityManager.UidFrozenStateChangedCallback.class);
+            verify(mActivityManager).registerUidFrozenStateChangedCallback(
+                    any(HandlerExecutor.class), frozenCaptor.capture());
+            mUidFrozenStateCallback = frozenCaptor.getValue();
+        }
+
         // Unable to mock mMockContext to return a mock stats manager.
         // So just mocking the whole MetricsHelper instance.
         mService.mMetricsHelper = mock(MetricsHelper.class);
@@ -3741,9 +3778,87 @@
         mListener.handleUidCachedChanged(TEST_CALLING_UID, true);
         assertAndHandleMessageSync(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED);
         assertEquals(3, mService.mAlarmsPerUid.get(TEST_CALLING_UID));
+        assertEquals(numExactListenerUid2 + 2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2));
 
         mListener.handleUidCachedChanged(TEST_CALLING_UID_2, true);
         assertAndHandleMessageSync(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED);
+        assertEquals(3, mService.mAlarmsPerUid.get(TEST_CALLING_UID));
+        assertEquals(2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2));
+    }
+
+    private void executeUidFrozenStateCallback(int[] uids, int[] frozenStates) {
+        assertNotNull(mUidFrozenStateCallback);
+        mUidFrozenStateCallback.onUidFrozenStateChanged(uids, frozenStates);
+    }
+
+    @EnableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS)
+    @Test
+    public void exactListenerAlarmsRemovedOnFrozen() {
+        mockChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, true);
+
+        setTestAlarmWithListener(ELAPSED_REALTIME, 31, getNewListener(() -> {}), WINDOW_EXACT,
+                TEST_CALLING_UID);
+        setTestAlarmWithListener(RTC, 42, getNewListener(() -> {}), 56, TEST_CALLING_UID);
+        setTestAlarm(ELAPSED_REALTIME, 54, WINDOW_EXACT, getNewMockPendingIntent(), 0, 0,
+                TEST_CALLING_UID, null);
+        setTestAlarm(RTC, 49, 154, getNewMockPendingIntent(), 0, 0, TEST_CALLING_UID, null);
+
+        setTestAlarmWithListener(ELAPSED_REALTIME, 21, getNewListener(() -> {}), WINDOW_EXACT,
+                TEST_CALLING_UID_2);
+        setTestAlarmWithListener(RTC, 412, getNewListener(() -> {}), 561, TEST_CALLING_UID_2);
+        setTestAlarm(ELAPSED_REALTIME, 26, WINDOW_EXACT, getNewMockPendingIntent(), 0, 0,
+                TEST_CALLING_UID_2, null);
+        setTestAlarm(RTC, 549, 234, getNewMockPendingIntent(), 0, 0, TEST_CALLING_UID_2, null);
+
+        assertEquals(8, mService.mAlarmStore.size());
+
+        executeUidFrozenStateCallback(
+                new int[] {TEST_CALLING_UID, TEST_CALLING_UID_2},
+                new int[] {UID_FROZEN_STATE_FROZEN, UID_FROZEN_STATE_UNFROZEN});
+        assertEquals(7, mService.mAlarmStore.size());
+
+        executeUidFrozenStateCallback(
+                new int[] {TEST_CALLING_UID_2}, new int[] {UID_FROZEN_STATE_FROZEN});
+        assertEquals(6, mService.mAlarmStore.size());
+    }
+
+    @EnableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS)
+    @Test
+    public void alarmCountOnListenerFrozen() {
+        mockChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, true);
+
+        // Set some alarms for TEST_CALLING_UID.
+        final int numExactListenerUid1 = 17;
+        for (int i = 0; i < numExactListenerUid1; i++) {
+            setTestAlarmWithListener(ALARM_TYPES[i % 4], mNowElapsedTest + i,
+                    getNewListener(() -> {}));
+        }
+        setTestAlarmWithListener(RTC, 42, getNewListener(() -> {}), 56, TEST_CALLING_UID);
+        setTestAlarm(ELAPSED_REALTIME, 54, getNewMockPendingIntent());
+        setTestAlarm(RTC, 49, 154, getNewMockPendingIntent(), 0, 0, TEST_CALLING_UID, null);
+
+        // Set some alarms for TEST_CALLING_UID_2.
+        final int numExactListenerUid2 = 11;
+        for (int i = 0; i < numExactListenerUid2; i++) {
+            setTestAlarmWithListener(ALARM_TYPES[i % 4], mNowElapsedTest + i,
+                    getNewListener(() -> {}), WINDOW_EXACT, TEST_CALLING_UID_2);
+        }
+        setTestAlarmWithListener(RTC, 412, getNewListener(() -> {}), 561, TEST_CALLING_UID_2);
+        setTestAlarm(RTC_WAKEUP, 26, WINDOW_EXACT, getNewMockPendingIntent(), 0, 0,
+                TEST_CALLING_UID_2, null);
+
+        assertEquals(numExactListenerUid1 + 3, mService.mAlarmsPerUid.get(TEST_CALLING_UID));
+        assertEquals(numExactListenerUid2 + 2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2));
+
+        executeUidFrozenStateCallback(
+                new int[] {TEST_CALLING_UID, TEST_CALLING_UID_2},
+                new int[] {UID_FROZEN_STATE_FROZEN, UID_FROZEN_STATE_UNFROZEN});
+        assertEquals(3, mService.mAlarmsPerUid.get(TEST_CALLING_UID));
+        assertEquals(numExactListenerUid2 + 2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2));
+
+        executeUidFrozenStateCallback(
+                new int[] {TEST_CALLING_UID_2}, new int[] {UID_FROZEN_STATE_FROZEN});
+        assertEquals(3, mService.mAlarmsPerUid.get(TEST_CALLING_UID));
         assertEquals(2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2));
     }
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
index 6cd79bc..ae6984e 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
@@ -18,12 +18,17 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
 import android.content.Context;
 import android.hardware.SensorManager;
+import android.os.AggregateBatteryConsumer;
 import android.os.BatteryConsumer;
 import android.os.BatteryManager;
 import android.os.BatteryStats;
@@ -34,6 +39,7 @@
 import android.os.Process;
 import android.os.UidBatteryConsumer;
 import android.platform.test.ravenwood.RavenwoodRule;
+import android.util.SparseLongArray;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -517,6 +523,57 @@
     }
 
     @Test
+    public void saveBatteryUsageStatsOnReset_incompatibleEnergyConsumers() throws Throwable {
+        MockBatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
+        batteryStats.initMeasuredEnergyStats(new String[]{"FOO", "BAR"});
+        int componentId0 = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
+        int componentId1 = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + 1;
+
+        synchronized (batteryStats) {
+            batteryStats.getUidStatsLocked(APP_UID);
+
+            SparseLongArray uidEnergies = new SparseLongArray();
+            uidEnergies.put(APP_UID, 30_000_000);
+            batteryStats.updateCustomEnergyConsumerStatsLocked(0, 100_000_000, uidEnergies);
+            batteryStats.updateCustomEnergyConsumerStatsLocked(1, 200_000_000, uidEnergies);
+        }
+
+        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null,
+                mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), null, mMockClock);
+
+        PowerStatsStore powerStatsStore = mock(PowerStatsStore.class);
+        doAnswer(invocation -> {
+            BatteryUsageStats stats = invocation.getArgument(1);
+            AggregateBatteryConsumer device = stats.getAggregateBatteryConsumer(
+                    BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE);
+            assertThat(device.getCustomPowerComponentName(componentId0)).isEqualTo("FOO");
+            assertThat(device.getCustomPowerComponentName(componentId1)).isEqualTo("BAR");
+            assertThat(device.getConsumedPowerForCustomComponent(componentId0))
+                    .isWithin(PRECISION).of(27.77777);
+            assertThat(device.getConsumedPowerForCustomComponent(componentId1))
+                    .isWithin(PRECISION).of(55.55555);
+
+            UidBatteryConsumer uid = stats.getUidBatteryConsumers().get(0);
+            assertThat(uid.getConsumedPowerForCustomComponent(componentId0))
+                    .isWithin(PRECISION).of(8.33333);
+            assertThat(uid.getConsumedPowerForCustomComponent(componentId1))
+                    .isWithin(PRECISION).of(8.33333);
+            return null;
+        }).when(powerStatsStore).storeBatteryUsageStats(anyLong(), any());
+
+        mStatsRule.getBatteryStats().saveBatteryUsageStatsOnReset(provider, powerStatsStore);
+
+        // Make an incompatible change of supported energy components.  This will trigger
+        // a BatteryStats reset, which will generate a snapshot of battery stats.
+        mStatsRule.initMeasuredEnergyStatsLocked(
+                new String[]{"COMPONENT1"});
+
+        mStatsRule.waitForBackgroundThread();
+
+        verify(powerStatsStore).storeBatteryUsageStats(anyLong(), any());
+    }
+
+    @Test
     public void testAggregateBatteryStats_incompatibleSnapshot() {
         MockBatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
         batteryStats.initMeasuredEnergyStats(new String[]{"FOO", "BAR"});
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
index 8bdb029..7e8fa55 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
@@ -16,6 +16,8 @@
 
 package com.android.server.power.stats;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.anyDouble;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
@@ -28,6 +30,7 @@
 import android.os.BatteryStats;
 import android.os.BatteryUsageStats;
 import android.os.BatteryUsageStatsQuery;
+import android.os.ConditionVariable;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.UidBatteryConsumer;
@@ -74,6 +77,7 @@
     private NetworkStats mNetworkStats;
     private boolean[] mSupportedStandardBuckets;
     private String[] mCustomPowerComponentNames;
+    private Throwable mThrowable;
 
     public BatteryUsageStatsRule() {
         this(0, null);
@@ -270,6 +274,7 @@
             public void evaluate() throws Throwable {
                 before();
                 base.evaluate();
+                after();
             }
         };
     }
@@ -277,6 +282,9 @@
     private void before() {
         lateInitBatteryStats();
         HandlerThread bgThread = new HandlerThread("bg thread");
+        bgThread.setUncaughtExceptionHandler((thread, throwable)-> {
+            mThrowable = throwable;
+        });
         bgThread.start();
         mHandler = new Handler(bgThread.getLooper());
         mBatteryStats.setHandler(mHandler);
@@ -285,6 +293,26 @@
         mBatteryStats.getOnBatteryScreenOffTimeBase().setRunning(!mScreenOn, 0, 0);
     }
 
+    private void after() throws Throwable {
+        if (mHandler != null) {
+            waitForBackgroundThread();
+        }
+    }
+
+    public void waitForBackgroundThread() throws Throwable {
+        if (mThrowable != null) {
+            throw mThrowable;
+        }
+
+        ConditionVariable done = new ConditionVariable();
+        mHandler.post(done::open);
+        assertThat(done.block(10000)).isTrue();
+
+        if (mThrowable != null) {
+            throw mThrowable;
+        }
+    }
+
     public PowerProfile getPowerProfile() {
         return mPowerProfile;
     }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt
index 52c7d8d..5c8c6bb 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt
@@ -17,31 +17,38 @@
 
 import android.hardware.display.DisplayManagerGlobal
 import android.os.SystemClock
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
 import android.view.Display
 import android.view.Display.DEFAULT_DISPLAY
 import android.view.DisplayAdjustments
 import android.view.DisplayInfo
 import android.view.IInputFilterHost
+import android.view.InputDevice.SOURCE_STYLUS
 import android.view.InputDevice.SOURCE_TOUCHSCREEN
 import android.view.InputEvent
 import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_CANCEL
 import android.view.MotionEvent.ACTION_DOWN
-import android.view.MotionEvent.ACTION_MOVE
-import android.view.MotionEvent.ACTION_UP
 import android.view.MotionEvent.ACTION_HOVER_ENTER
 import android.view.MotionEvent.ACTION_HOVER_EXIT
 import android.view.MotionEvent.ACTION_HOVER_MOVE
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_UP
 import android.view.WindowManagerPolicyConstants.FLAG_PASS_TO_USER
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.cts.input.inputeventmatchers.withDeviceId
 import com.android.cts.input.inputeventmatchers.withMotionAction
+import com.android.cts.input.inputeventmatchers.withSource
 import com.android.server.LocalServices
 import com.android.server.accessibility.magnification.MagnificationProcessor
 import com.android.server.wm.WindowManagerInternal
 import java.util.concurrent.LinkedBlockingQueue
 import org.hamcrest.Matchers.allOf
 import org.junit.After
+import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -92,12 +99,17 @@
                 or AccessibilityInputFilter.FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER
                 or AccessibilityInputFilter.FLAG_FEATURE_INJECT_MOTION_EVENTS
                 or AccessibilityInputFilter.FLAG_FEATURE_FILTER_KEY_EVENTS)
+        const val STYLUS_SOURCE = SOURCE_STYLUS or SOURCE_TOUCHSCREEN
     }
 
     @Rule
     @JvmField
     val mocks: MockitoRule = MockitoJUnit.rule()
 
+    @Rule
+    @JvmField
+    val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
     @Mock
     private lateinit var mockA11yController: WindowManagerInternal.AccessibilityControllerInternal
 
@@ -115,6 +127,9 @@
     private lateinit var ams: AccessibilityManagerService
     private lateinit var a11yInputFilter: AccessibilityInputFilter
     private val touchDeviceId = 1
+    private val fromTouchScreen = allOf(withDeviceId(touchDeviceId), withSource(SOURCE_TOUCHSCREEN))
+    private val stylusDeviceId = 2
+    private val fromStylus = allOf(withDeviceId(stylusDeviceId), withSource(STYLUS_SOURCE))
 
     @Before
     fun setUp() {
@@ -156,23 +171,14 @@
         enableFeatures(0)
 
         val downTime = SystemClock.uptimeMillis()
-        val downEvent = createMotionEvent(
-            ACTION_DOWN, downTime, downTime, SOURCE_TOUCHSCREEN, touchDeviceId)
-        send(downEvent)
-        verifier.assertReceivedMotion(
-            allOf(withMotionAction(ACTION_DOWN), withDeviceId(touchDeviceId)))
+        sendTouchEvent(ACTION_DOWN, downTime, downTime)
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_DOWN)))
 
-        val moveEvent = createMotionEvent(
-            ACTION_MOVE, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
-        send(moveEvent)
-        verifier.assertReceivedMotion(
-            allOf(withMotionAction(ACTION_MOVE), withDeviceId(touchDeviceId)))
+        sendTouchEvent(ACTION_MOVE, downTime, SystemClock.uptimeMillis())
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_MOVE)))
 
-        val upEvent = createMotionEvent(
-            ACTION_UP, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
-        send(upEvent)
-        verifier.assertReceivedMotion(
-            allOf(withMotionAction(ACTION_UP), withDeviceId(touchDeviceId)))
+        sendTouchEvent(ACTION_UP, downTime, SystemClock.uptimeMillis())
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_UP)))
 
         verifier.assertNoEvents()
     }
@@ -186,28 +192,91 @@
         enableFeatures(ALL_A11Y_FEATURES)
 
         val downTime = SystemClock.uptimeMillis()
-        val downEvent = createMotionEvent(
-            ACTION_DOWN, downTime, downTime, SOURCE_TOUCHSCREEN, touchDeviceId)
-        send(MotionEvent.obtain(downEvent))
-
+        sendTouchEvent(ACTION_DOWN, downTime, downTime)
         // DOWN event gets transformed to HOVER_ENTER
-        verifier.assertReceivedMotion(
-            allOf(withMotionAction(ACTION_HOVER_ENTER), withDeviceId(touchDeviceId)))
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_ENTER)))
 
         // MOVE becomes HOVER_MOVE
-        val moveEvent = createMotionEvent(
-            ACTION_MOVE, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
-        send(moveEvent)
-        verifier.assertReceivedMotion(
-            allOf(withMotionAction(ACTION_HOVER_MOVE), withDeviceId(touchDeviceId)))
+        sendTouchEvent(ACTION_MOVE, downTime, SystemClock.uptimeMillis())
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_MOVE)))
 
         // UP becomes HOVER_EXIT
-        val upEvent = createMotionEvent(
-            ACTION_UP, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
-        send(upEvent)
+        sendTouchEvent(ACTION_UP, downTime, SystemClock.uptimeMillis())
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_EXIT)))
 
-        verifier.assertReceivedMotion(
-            allOf(withMotionAction(ACTION_HOVER_EXIT), withDeviceId(touchDeviceId)))
+        verifier.assertNoEvents()
+    }
+
+    /**
+     * Enable all a11y features and send a touchscreen stream of DOWN -> CANCEL -> DOWN events.
+     * These get converted into HOVER_ENTER -> HOVER_EXIT -> HOVER_ENTER events by the input filter.
+     */
+    @Test
+    fun testTouchDownCancelDownWithAllA11yFeatures() {
+        enableFeatures(ALL_A11Y_FEATURES)
+
+        val downTime = SystemClock.uptimeMillis()
+        sendTouchEvent(ACTION_DOWN, downTime, downTime)
+        // DOWN event gets transformed to HOVER_ENTER
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_ENTER)))
+
+        // CANCEL becomes HOVER_EXIT
+        sendTouchEvent(ACTION_CANCEL, downTime, SystemClock.uptimeMillis())
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_EXIT)))
+
+        // DOWN again! New hover is expected
+        val newDownTime = SystemClock.uptimeMillis()
+        sendTouchEvent(ACTION_DOWN, newDownTime, newDownTime)
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_ENTER)))
+
+        verifier.assertNoEvents()
+    }
+
+    /**
+     * Enable all a11y features and send a stylus stream of DOWN -> CANCEL -> DOWN events.
+     * These get converted into HOVER_ENTER -> HOVER_EXIT -> HOVER_ENTER events by the input filter.
+     * This test is the same as above, but for stylus events.
+     */
+    @Test
+    fun testStylusDownCancelDownWithAllA11yFeatures() {
+        enableFeatures(ALL_A11Y_FEATURES)
+
+        val downTime = SystemClock.uptimeMillis()
+        sendStylusEvent(ACTION_DOWN, downTime, downTime)
+        // DOWN event gets transformed to HOVER_ENTER
+        verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_ENTER)))
+
+        // CANCEL becomes HOVER_EXIT
+        sendStylusEvent(ACTION_CANCEL, downTime, SystemClock.uptimeMillis())
+        verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_EXIT)))
+
+        // DOWN again! New hover is expected
+        val newDownTime = SystemClock.uptimeMillis()
+        sendStylusEvent(ACTION_DOWN, newDownTime, newDownTime)
+        verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_ENTER)))
+
+        verifier.assertNoEvents()
+    }
+
+    /**
+     * Enable all a11y features and send a stylus stream and then a touch stream.
+     */
+    @Test
+    fun testStylusThenTouch() {
+        enableFeatures(ALL_A11Y_FEATURES)
+
+        val downTime = SystemClock.uptimeMillis()
+        sendStylusEvent(ACTION_DOWN, downTime, downTime)
+        // DOWN event gets transformed to HOVER_ENTER
+        verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_ENTER)))
+
+        // CANCEL becomes HOVER_EXIT
+        sendStylusEvent(ACTION_CANCEL, downTime, SystemClock.uptimeMillis())
+        verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_EXIT)))
+
+        val newDownTime = SystemClock.uptimeMillis()
+        sendTouchEvent(ACTION_DOWN, newDownTime, newDownTime)
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_ENTER)))
 
         verifier.assertNoEvents()
     }
@@ -223,26 +292,18 @@
         enableFeatures(ALL_A11Y_FEATURES)
 
         val downTime = SystemClock.uptimeMillis()
-        val downEvent = createMotionEvent(
-            ACTION_DOWN, downTime, downTime, SOURCE_TOUCHSCREEN, touchDeviceId)
-        send(MotionEvent.obtain(downEvent))
+        sendTouchEvent(ACTION_DOWN, downTime, downTime)
 
         // DOWN event gets transformed to HOVER_ENTER
-        verifier.assertReceivedMotion(
-            allOf(withMotionAction(ACTION_HOVER_ENTER), withDeviceId(touchDeviceId)))
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_ENTER)))
         verifier.assertNoEvents()
 
         enableFeatures(0)
-        verifier.assertReceivedMotion(
-            allOf(withMotionAction(ACTION_HOVER_EXIT), withDeviceId(touchDeviceId)))
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_EXIT)))
         verifier.assertNoEvents()
 
-        val moveEvent = createMotionEvent(
-            ACTION_MOVE, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
-        send(moveEvent)
-        val upEvent = createMotionEvent(
-            ACTION_UP, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
-        send(upEvent)
+        sendTouchEvent(ACTION_MOVE, downTime, SystemClock.uptimeMillis())
+        sendTouchEvent(ACTION_UP, downTime, SystemClock.uptimeMillis())
         // As the original gesture continues, no additional events should be getting sent by the
         // filter because the HOVER_EXIT above already effectively finished the current gesture and
         // the DOWN event was never sent to the host.
@@ -250,10 +311,148 @@
         // Bug: the down event was swallowed, so the remainder of the gesture should be swallowed
         // too. However, the MOVE and UP events are currently passed back to the dispatcher.
         // TODO(b/310014874) - ensure a11y sends consistent input streams to the dispatcher
-        verifier.assertReceivedMotion(
-            allOf(withMotionAction(ACTION_MOVE), withDeviceId(touchDeviceId)))
-        verifier.assertReceivedMotion(
-            allOf(withMotionAction(ACTION_UP), withDeviceId(touchDeviceId)))
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_MOVE)))
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_UP)))
+
+        verifier.assertNoEvents()
+    }
+
+    /**
+     * Check multi-device behaviour when all a11y features are disabled. The events should pass
+     * through unmodified, but only from the active (first) device.
+     * The events from the inactive device should be dropped.
+     * In this test, we are injecting a touchscreen event stream and a stylus event stream,
+     * interleaved.
+     */
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_HANDLE_MULTI_DEVICE_INPUT)
+    fun testMultiDeviceEventsWithoutA11yFeatures() {
+        enableFeatures(0)
+
+        val touchDownTime = SystemClock.uptimeMillis()
+
+        // Touch device - ACTION_DOWN
+        sendTouchEvent(ACTION_DOWN, touchDownTime, touchDownTime)
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_DOWN)))
+
+        // Stylus device - ACTION_DOWN
+        val stylusDownTime = SystemClock.uptimeMillis()
+        sendStylusEvent(ACTION_DOWN, stylusDownTime, stylusDownTime)
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_CANCEL)))
+        verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_DOWN)))
+
+        // Touch device - ACTION_MOVE
+        sendTouchEvent(ACTION_MOVE, touchDownTime, SystemClock.uptimeMillis())
+        // Touch event is dropped
+        verifier.assertNoEvents()
+
+        // Stylus device - ACTION_MOVE
+        sendStylusEvent(ACTION_MOVE, stylusDownTime, SystemClock.uptimeMillis())
+        verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_MOVE)))
+
+        // Touch device - ACTION_UP
+        sendTouchEvent(ACTION_UP, touchDownTime, SystemClock.uptimeMillis())
+        // Touch event is dropped
+        verifier.assertNoEvents()
+
+        // Stylus device - ACTION_UP
+        sendStylusEvent(ACTION_UP, stylusDownTime, SystemClock.uptimeMillis())
+        verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_UP)))
+
+        verifier.assertNoEvents()
+    }
+
+    /**
+     * Check multi-device behaviour when all a11y features are enabled. The events should be
+     * modified accordingly, like DOWN events getting converted to hovers.
+     * Only a single device should be active (the latest device to start a new gesture).
+     * In this test, we are injecting a touchscreen event stream and a stylus event stream,
+     * interleaved.
+     */
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_HANDLE_MULTI_DEVICE_INPUT)
+    fun testMultiDeviceEventsWithAllA11yFeatures() {
+        enableFeatures(ALL_A11Y_FEATURES)
+
+        // Touch device - ACTION_DOWN
+        val touchDownTime = SystemClock.uptimeMillis()
+        sendTouchEvent(ACTION_DOWN, touchDownTime, touchDownTime)
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_ENTER)))
+
+        // Stylus device - ACTION_DOWN
+        val stylusDownTime = SystemClock.uptimeMillis()
+        sendStylusEvent(ACTION_DOWN, stylusDownTime, stylusDownTime)
+        // Touch is canceled and stylus is started
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_EXIT)))
+        verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_ENTER)))
+
+        // Touch device - ACTION_MOVE
+        sendTouchEvent(ACTION_MOVE, touchDownTime, SystemClock.uptimeMillis())
+        // Stylus is active now; touch is ignored
+        verifier.assertNoEvents()
+
+        // Stylus device - ACTION_MOVE
+        sendStylusEvent(ACTION_MOVE, stylusDownTime, SystemClock.uptimeMillis())
+        verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_MOVE)))
+
+        // Touch device - ACTION_UP
+        sendTouchEvent(ACTION_UP, touchDownTime, SystemClock.uptimeMillis())
+        // Stylus is still active; touch is ignored
+        verifier.assertNoEvents()
+
+        sendStylusEvent(ACTION_UP, stylusDownTime, SystemClock.uptimeMillis())
+        verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_EXIT)))
+
+        // Now stylus is done, and a new touch gesture will work!
+        val newTouchDownTime = SystemClock.uptimeMillis()
+        sendTouchEvent(ACTION_DOWN, newTouchDownTime, newTouchDownTime)
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_ENTER)))
+
+        verifier.assertNoEvents()
+    }
+
+    /**
+     * Check multi-device behaviour when all a11y features are enabled. The events should be
+     * modified accordingly, like DOWN events getting converted to hovers.
+     * Only a single device should be active at a given time. The touch events start and end
+     * while stylus is active. Check that the latest device is always given preference.
+     */
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_HANDLE_MULTI_DEVICE_INPUT)
+    fun testStylusWithTouchInTheMiddle() {
+        enableFeatures(ALL_A11Y_FEATURES)
+
+        // Stylus device - ACTION_DOWN
+        val stylusDownTime = SystemClock.uptimeMillis()
+        sendStylusEvent(ACTION_DOWN, stylusDownTime, stylusDownTime)
+        verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_ENTER)))
+
+        // Touch device - ACTION_DOWN
+        val touchDownTime = SystemClock.uptimeMillis()
+        sendTouchEvent(ACTION_DOWN, touchDownTime, touchDownTime)
+        // Touch DOWN causes stylus to get canceled
+        verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_EXIT)))
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_ENTER)))
+
+        // Touch device - ACTION_MOVE
+        sendTouchEvent(ACTION_MOVE, touchDownTime, SystemClock.uptimeMillis())
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_MOVE)))
+
+        sendStylusEvent(ACTION_MOVE, stylusDownTime, SystemClock.uptimeMillis())
+        // Stylus is ignored because touch is active now
+        verifier.assertNoEvents()
+
+        sendTouchEvent(ACTION_UP, touchDownTime, SystemClock.uptimeMillis())
+        verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_EXIT)))
+
+        sendStylusEvent(ACTION_UP, stylusDownTime, SystemClock.uptimeMillis())
+        // The UP stylus event is also ignored
+        verifier.assertNoEvents()
+
+        // Now stylus works again, because touch gesture is finished
+        val newStylusDownTime = SystemClock.uptimeMillis()
+        sendStylusEvent(ACTION_DOWN, newStylusDownTime, newStylusDownTime)
+        verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_ENTER)))
 
         verifier.assertNoEvents()
     }
@@ -264,6 +463,20 @@
         return display
     }
 
+    private fun sendTouchEvent(action: Int, downTime: Long, eventTime: Long) {
+        if (action == ACTION_DOWN) {
+            assertEquals(downTime, eventTime)
+        }
+        send(createMotionEvent(action, downTime, eventTime, SOURCE_TOUCHSCREEN, touchDeviceId))
+    }
+
+    private fun sendStylusEvent(action: Int, downTime: Long, eventTime: Long) {
+        if (action == ACTION_DOWN) {
+            assertEquals(downTime, eventTime)
+        }
+        send(createMotionEvent(action, downTime, eventTime, STYLUS_SOURCE, stylusDeviceId))
+    }
+
     private fun send(event: InputEvent) {
         // We need to make a copy of the event before sending it to the filter, because the filter
         // will recycle it, but the caller of this function might want to still be able to use
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
index b322dd7..aec3f45 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
@@ -17,6 +17,7 @@
 package com.android.server.accessibility;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -33,17 +34,24 @@
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.internal.util.HexDump;
+
 import com.google.common.truth.Expect;
 
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.io.File;
 import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -51,184 +59,265 @@
  *
  * <p>Prefer adding new tests in CTS where possible.
  */
+@RunWith(Enclosed.class)
 public class BrailleDisplayConnectionTest {
-    private static final Path NULL_PATH = Path.of("/dev/null");
 
-    private BrailleDisplayConnection mBrailleDisplayConnection;
-    @Mock
-    private BrailleDisplayConnection.NativeInterface mNativeInterface;
-    @Mock
-    private AccessibilityServiceConnection mServiceConnection;
+    public static class ScannerTest {
+        private static final Path NULL_PATH = Path.of("/dev/null");
 
-    @Rule
-    public final Expect expect = Expect.create();
+        private BrailleDisplayConnection mBrailleDisplayConnection;
+        @Mock
+        private BrailleDisplayConnection.NativeInterface mNativeInterface;
+        @Mock
+        private AccessibilityServiceConnection mServiceConnection;
 
-    private Context mContext;
+        @Rule
+        public final Expect expect = Expect.create();
 
-    // To mock package-private class
-    @Rule
-    public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
-            new DexmakerShareClassLoaderRule();
+        private Context mContext;
 
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mContext = InstrumentationRegistry.getInstrumentation().getContext();
-        when(mServiceConnection.isConnectedLocked()).thenReturn(true);
-        mBrailleDisplayConnection =
-                spy(new BrailleDisplayConnection(new Object(), mServiceConnection));
-    }
+        // To mock package-private class
+        @Rule
+        public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
+                new DexmakerShareClassLoaderRule();
 
-    @Test
-    public void defaultNativeScanner_getHidrawNodePaths_returnsHidrawPaths() throws Exception {
-        File testDir = mContext.getFilesDir();
-        Path hidrawNode0 = Path.of(testDir.getPath(), "hidraw0");
-        Path hidrawNode1 = Path.of(testDir.getPath(), "hidraw1");
-        Path otherDevice = Path.of(testDir.getPath(), "otherDevice");
-        Path[] nodePaths = {hidrawNode0, hidrawNode1, otherDevice};
-        try {
-            for (Path node : nodePaths) {
-                assertThat(node.toFile().createNewFile()).isTrue();
+        @Before
+        public void setup() {
+            MockitoAnnotations.initMocks(this);
+            mContext = InstrumentationRegistry.getInstrumentation().getContext();
+            when(mServiceConnection.isConnectedLocked()).thenReturn(true);
+            mBrailleDisplayConnection =
+                    spy(new BrailleDisplayConnection(new Object(), mServiceConnection));
+        }
+
+        @Test
+        public void defaultNativeScanner_getHidrawNodePaths_returnsHidrawPaths() throws Exception {
+            File testDir = mContext.getFilesDir();
+            Path hidrawNode0 = Path.of(testDir.getPath(), "hidraw0");
+            Path hidrawNode1 = Path.of(testDir.getPath(), "hidraw1");
+            Path otherDevice = Path.of(testDir.getPath(), "otherDevice");
+            Path[] nodePaths = {hidrawNode0, hidrawNode1, otherDevice};
+            try {
+                for (Path node : nodePaths) {
+                    assertThat(node.toFile().createNewFile()).isTrue();
+                }
+
+                BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                        mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+                assertThat(scanner.getHidrawNodePaths(testDir.toPath()))
+                        .containsExactly(hidrawNode0, hidrawNode1);
+            } finally {
+                for (Path node : nodePaths) {
+                    node.toFile().delete();
+                }
             }
+        }
+
+        @Test
+        public void defaultNativeScanner_getReportDescriptor_returnsDescriptor() {
+            int descriptorSize = 4;
+            byte[] descriptor = {0xB, 0xE, 0xE, 0xF};
+            when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(descriptorSize);
+            when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn(
+                    descriptor);
 
             BrailleDisplayConnection.BrailleDisplayScanner scanner =
                     mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
 
-            assertThat(scanner.getHidrawNodePaths(testDir.toPath()))
-                    .containsExactly(hidrawNode0, hidrawNode1);
-        } finally {
-            for (Path node : nodePaths) {
-                node.toFile().delete();
-            }
+            assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isEqualTo(descriptor);
+        }
+
+        @Test
+        public void defaultNativeScanner_getReportDescriptor_invalidSize_returnsNull() {
+            when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(0);
+
+            BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                    mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+            assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isNull();
+        }
+
+        @Test
+        public void defaultNativeScanner_getUniqueId_returnsUniq() {
+            String macAddress = "12:34:56:78";
+            when(mNativeInterface.getHidrawUniq(anyInt())).thenReturn(macAddress);
+
+            BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                    mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+            assertThat(scanner.getUniqueId(NULL_PATH)).isEqualTo(macAddress);
+        }
+
+        @Test
+        public void defaultNativeScanner_getDeviceBusType_busUsb() {
+            when(mNativeInterface.getHidrawBusType(anyInt()))
+                    .thenReturn(BrailleDisplayConnection.BUS_USB);
+
+            BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                    mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+            assertThat(scanner.getDeviceBusType(NULL_PATH))
+                    .isEqualTo(BrailleDisplayConnection.BUS_USB);
+        }
+
+        @Test
+        public void defaultNativeScanner_getDeviceBusType_busBluetooth() {
+            when(mNativeInterface.getHidrawBusType(anyInt()))
+                    .thenReturn(BrailleDisplayConnection.BUS_BLUETOOTH);
+
+            BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                    mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+            assertThat(scanner.getDeviceBusType(NULL_PATH))
+                    .isEqualTo(BrailleDisplayConnection.BUS_BLUETOOTH);
+        }
+
+        @Test
+        public void write_bypassesServiceSideCheckWithLargeBuffer_disconnects() {
+            Mockito.doNothing().when(mBrailleDisplayConnection).disconnect();
+            mBrailleDisplayConnection.write(
+                    new byte[IBinder.getSuggestedMaxIpcSizeBytes() * 2]);
+
+            verify(mBrailleDisplayConnection).disconnect();
+        }
+
+        @Test
+        public void write_notConnected_throwsIllegalStateException() {
+            when(mServiceConnection.isConnectedLocked()).thenReturn(false);
+
+            assertThrows(IllegalStateException.class,
+                    () -> mBrailleDisplayConnection.write(new byte[1]));
+        }
+
+        @Test
+        public void write_unableToCreateWriteStream_disconnects() {
+            Mockito.doNothing().when(mBrailleDisplayConnection).disconnect();
+            // mBrailleDisplayConnection#connectLocked was never called so the
+            // connection's mHidrawNode is still null. This will throw an exception
+            // when attempting to create FileOutputStream on the node.
+            mBrailleDisplayConnection.write(new byte[1]);
+
+            verify(mBrailleDisplayConnection).disconnect();
+        }
+
+        // BrailleDisplayConnection#setTestData() is used to enable CTS testing with
+        // test Braille display data, but its own implementation should also be tested
+        // so that issues in this helper don't cause confusing failures in CTS.
+
+        @Test
+        public void setTestData_scannerReturnsTestData() {
+            Bundle bd1 = new Bundle(), bd2 = new Bundle();
+
+            Path path1 = Path.of("/dev/path1"), path2 = Path.of("/dev/path2");
+            bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH,
+                    path1.toString());
+            bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH,
+                    path2.toString());
+            byte[] desc1 = {0xB, 0xE}, desc2 = {0xE, 0xF};
+            bd1.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc1);
+            bd2.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc2);
+            String uniq1 = "uniq1", uniq2 = "uniq2";
+            bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq1);
+            bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq2);
+            int bus1 = BrailleDisplayConnection.BUS_USB, bus2 =
+                    BrailleDisplayConnection.BUS_BLUETOOTH;
+            bd1.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
+                    bus1 == BrailleDisplayConnection.BUS_BLUETOOTH);
+            bd2.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
+                    bus2 == BrailleDisplayConnection.BUS_BLUETOOTH);
+
+            BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                    mBrailleDisplayConnection.setTestData(List.of(bd1, bd2));
+
+            expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).containsExactly(path1, path2);
+            expect.that(scanner.getDeviceReportDescriptor(path1)).isEqualTo(desc1);
+            expect.that(scanner.getDeviceReportDescriptor(path2)).isEqualTo(desc2);
+            expect.that(scanner.getUniqueId(path1)).isEqualTo(uniq1);
+            expect.that(scanner.getUniqueId(path2)).isEqualTo(uniq2);
+            expect.that(scanner.getDeviceBusType(path1)).isEqualTo(bus1);
+            expect.that(scanner.getDeviceBusType(path2)).isEqualTo(bus2);
+        }
+
+        @Test
+        public void setTestData_emptyTestData_returnsNullNodePaths() {
+            BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                    mBrailleDisplayConnection.setTestData(List.of());
+
+            expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).isNull();
         }
     }
 
-    @Test
-    public void defaultNativeScanner_getReportDescriptor_returnsDescriptor() {
-        int descriptorSize = 4;
-        byte[] descriptor = {0xB, 0xE, 0xE, 0xF};
-        when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(descriptorSize);
-        when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn(descriptor);
+    @RunWith(Parameterized.class)
+    public static class BrailleDisplayDescriptorTest {
+        @Parameterized.Parameters(name = "{0}")
+        public static Collection<Object[]> data() {
+            return Arrays.asList(new Object[][]{
+                    {"match_BdPage", new byte[]{
+                            // Just one item, defines the BD page
+                            0x05, 0x41}},
+                    {"match_BdPageAfterAnotherPage", new byte[]{
+                            // One item defines another page
+                            0x05, 0x01,
+                            // Next item defines BD page
+                            0x05, 0x41}},
+                    {"match_BdPageAfterSizeZeroItem", new byte[]{
+                            // Size-zero item (last 2 bits are 00)
+                            0x00,
+                            // Next item defines BD page
+                            0x05, 0x41}},
+                    {"match_BdPageAfterSizeOneItem", new byte[]{
+                            // Size-one item (last 2 bits are 01)
+                            0x01, 0x7F,
+                            // Next item defines BD page
+                            0x05, 0x41}},
+                    {"match_BdPageAfterSizeTwoItem", new byte[]{
+                            // Size-two item (last 2 bits are 10)
+                            0x02, 0x7F, 0x7F,
+                            0x05, 0x41}},
+                    {"match_BdPageAfterSizeFourItem", new byte[]{
+                            // Size-four item (last 2 bits are 11)
+                            0x03, 0x7F, 0x7F, 0x7F, 0x7F,
+                            0x05, 0x41}},
+                    {"match_BdPageInBetweenOtherPages", new byte[]{
+                            // One item defines another page
+                            0x05, 0x01,
+                            // Next item defines BD page
+                            0x05, 0x41,
+                            // Next item defines another page
+                            0x05, 0x02}},
+                    {"fail_OtherPage", new byte[]{
+                            // Just one item, defines another page
+                            0x05, 0x01}},
+                    {"fail_BdPageBeforeMissingData", new byte[]{
+                            // This item defines BD page
+                            0x05, 0x41,
+                            // Next item specifies size-one item (last 2 bits are 01) but
+                            // that one data byte is missing; this descriptor is malformed.
+                            0x01}},
+                    {"fail_BdPageWithWrongDataSize", new byte[]{
+                            // This item defines a page with two-byte ID 0x41 0x7F, not 0x41.
+                            0x06, 0x41, 0x7F}},
+                    {"fail_LongItem", new byte[]{
+                            // Item has type bits 1111, indicating Long Item.
+                            (byte) 0xF0}},
+            });
+        }
 
-        BrailleDisplayConnection.BrailleDisplayScanner scanner =
-                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
 
-        assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isEqualTo(descriptor);
-    }
+        @Parameterized.Parameter(0)
+        public String mTestName;
+        @Parameterized.Parameter(1)
+        public byte[] mDescriptor;
 
-    @Test
-    public void defaultNativeScanner_getReportDescriptor_invalidSize_returnsNull() {
-        when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(0);
-
-        BrailleDisplayConnection.BrailleDisplayScanner scanner =
-                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
-
-        assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isNull();
-    }
-
-    @Test
-    public void defaultNativeScanner_getUniqueId_returnsUniq() {
-        String macAddress = "12:34:56:78";
-        when(mNativeInterface.getHidrawUniq(anyInt())).thenReturn(macAddress);
-
-        BrailleDisplayConnection.BrailleDisplayScanner scanner =
-                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
-
-        assertThat(scanner.getUniqueId(NULL_PATH)).isEqualTo(macAddress);
-    }
-
-    @Test
-    public void defaultNativeScanner_getDeviceBusType_busUsb() {
-        when(mNativeInterface.getHidrawBusType(anyInt()))
-                .thenReturn(BrailleDisplayConnection.BUS_USB);
-
-        BrailleDisplayConnection.BrailleDisplayScanner scanner =
-                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
-
-        assertThat(scanner.getDeviceBusType(NULL_PATH))
-                .isEqualTo(BrailleDisplayConnection.BUS_USB);
-    }
-
-    @Test
-    public void defaultNativeScanner_getDeviceBusType_busBluetooth() {
-        when(mNativeInterface.getHidrawBusType(anyInt()))
-                .thenReturn(BrailleDisplayConnection.BUS_BLUETOOTH);
-
-        BrailleDisplayConnection.BrailleDisplayScanner scanner =
-                mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
-
-        assertThat(scanner.getDeviceBusType(NULL_PATH))
-                .isEqualTo(BrailleDisplayConnection.BUS_BLUETOOTH);
-    }
-
-    @Test
-    public void write_bypassesServiceSideCheckWithLargeBuffer_disconnects() {
-        Mockito.doNothing().when(mBrailleDisplayConnection).disconnect();
-        mBrailleDisplayConnection.write(
-                new byte[IBinder.getSuggestedMaxIpcSizeBytes() * 2]);
-
-        verify(mBrailleDisplayConnection).disconnect();
-    }
-
-    @Test
-    public void write_notConnected_throwsIllegalStateException() {
-        when(mServiceConnection.isConnectedLocked()).thenReturn(false);
-
-        assertThrows(IllegalStateException.class,
-                () -> mBrailleDisplayConnection.write(new byte[1]));
-    }
-
-    @Test
-    public void write_unableToCreateWriteStream_disconnects() {
-        Mockito.doNothing().when(mBrailleDisplayConnection).disconnect();
-        // mBrailleDisplayConnection#connectLocked was never called so the
-        // connection's mHidrawNode is still null. This will throw an exception
-        // when attempting to create FileOutputStream on the node.
-        mBrailleDisplayConnection.write(new byte[1]);
-
-        verify(mBrailleDisplayConnection).disconnect();
-    }
-
-    // BrailleDisplayConnection#setTestData() is used to enable CTS testing with
-    // test Braille display data, but its own implementation should also be tested
-    // so that issues in this helper don't cause confusing failures in CTS.
-
-    @Test
-    public void setTestData_scannerReturnsTestData() {
-        Bundle bd1 = new Bundle(), bd2 = new Bundle();
-
-        Path path1 = Path.of("/dev/path1"), path2 = Path.of("/dev/path2");
-        bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path1.toString());
-        bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path2.toString());
-        byte[] desc1 = {0xB, 0xE}, desc2 = {0xE, 0xF};
-        bd1.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc1);
-        bd2.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc2);
-        String uniq1 = "uniq1", uniq2 = "uniq2";
-        bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq1);
-        bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq2);
-        int bus1 = BrailleDisplayConnection.BUS_USB, bus2 = BrailleDisplayConnection.BUS_BLUETOOTH;
-        bd1.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
-                bus1 == BrailleDisplayConnection.BUS_BLUETOOTH);
-        bd2.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
-                bus2 == BrailleDisplayConnection.BUS_BLUETOOTH);
-
-        BrailleDisplayConnection.BrailleDisplayScanner scanner =
-                mBrailleDisplayConnection.setTestData(List.of(bd1, bd2));
-
-        expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).containsExactly(path1, path2);
-        expect.that(scanner.getDeviceReportDescriptor(path1)).isEqualTo(desc1);
-        expect.that(scanner.getDeviceReportDescriptor(path2)).isEqualTo(desc2);
-        expect.that(scanner.getUniqueId(path1)).isEqualTo(uniq1);
-        expect.that(scanner.getUniqueId(path2)).isEqualTo(uniq2);
-        expect.that(scanner.getDeviceBusType(path1)).isEqualTo(bus1);
-        expect.that(scanner.getDeviceBusType(path2)).isEqualTo(bus2);
-    }
-
-    @Test
-    public void setTestData_emptyTestData_returnsNullNodePaths() {
-        BrailleDisplayConnection.BrailleDisplayScanner scanner =
-                mBrailleDisplayConnection.setTestData(List.of());
-
-        expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).isNull();
+        @Test
+        public void isBrailleDisplay() {
+            final boolean expectedMatch = mTestName.startsWith("match_");
+            assertWithMessage(
+                    "Expected isBrailleDisplay==" + expectedMatch
+                            + " for descriptor " + HexDump.toHexString(mDescriptor))
+                    .that(BrailleDisplayConnection.isBrailleDisplay(mDescriptor))
+                    .isEqualTo(expectedMatch);
+        }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
index 81df597..3e748ff 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -75,6 +75,7 @@
 import android.content.pm.SigningInfo;
 import android.content.pm.UserInfo;
 import android.content.pm.UserPackage;
+import android.content.pm.UserProperties;
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
 import android.graphics.drawable.Icon;
@@ -776,6 +777,15 @@
             new UserInfo(USER_P1, "userP1",
                     UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_MANAGED_PROFILE), 0);
 
+    protected static final UserProperties USER_PROPERTIES_0 =
+            new UserProperties.Builder().setItemsRestrictedOnHomeScreen(false).build();
+
+    protected static final UserProperties USER_PROPERTIES_10 =
+            new UserProperties.Builder().setItemsRestrictedOnHomeScreen(false).build();
+
+    protected static final UserProperties USER_PROPERTIES_11 =
+            new UserProperties.Builder().setItemsRestrictedOnHomeScreen(true).build();
+
     protected BiPredicate<String, Integer> mDefaultLauncherChecker =
             (callingPackage, userId) ->
             LAUNCHER_1.equals(callingPackage) || LAUNCHER_2.equals(callingPackage)
@@ -817,6 +827,7 @@
             = new HashMap<>();
 
     protected final Map<Integer, UserInfo> mUserInfos = new HashMap<>();
+    protected final Map<Integer, UserProperties> mUserProperties = new HashMap<>();
     protected final Map<Integer, Boolean> mRunningUsers = new HashMap<>();
     protected final Map<Integer, Boolean> mUnlockedUsers = new HashMap<>();
 
@@ -911,6 +922,9 @@
         mUserInfos.put(USER_11, USER_INFO_11);
         mUserInfos.put(USER_P0, USER_INFO_P0);
         mUserInfos.put(USER_P1, USER_INFO_P1);
+        mUserProperties.put(USER_0, USER_PROPERTIES_0);
+        mUserProperties.put(USER_10, USER_PROPERTIES_10);
+        mUserProperties.put(USER_11, USER_PROPERTIES_11);
 
         when(mMockUserManagerInternal.isUserUnlockingOrUnlocked(anyInt()))
                 .thenAnswer(inv -> {
@@ -959,6 +973,15 @@
                 inv -> mUserInfos.get((Integer) inv.getArguments()[0])));
         when(mMockActivityManagerInternal.getUidProcessState(anyInt())).thenReturn(
                 ActivityManager.PROCESS_STATE_CACHED_EMPTY);
+        when(mMockUserManagerInternal.getUserProperties(anyInt()))
+                .thenAnswer(inv -> {
+                    final int userId = (Integer) inv.getArguments()[0];
+                    final UserProperties userProperties = mUserProperties.get(userId);
+                    if (userProperties == null) {
+                        return new UserProperties.Builder().build();
+                    }
+                    return userProperties;
+                });
 
         // User 0 and P0 are always running
         mRunningUsers.put(USER_0, true);