Merge "Do not set dispose callback when flag is enabled" into main
diff --git a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
index 978a8f9..e3dbb2b 100644
--- a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
+++ b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
@@ -747,6 +747,10 @@
      * {@link android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession#createHighSpeedRequestList}.
      * </p>
      *
+     * <p>This function returns an empty array if
+     * {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO}
+     * is not supported.</p>
+     *
      * @return an array of supported high speed video recording sizes
      * @see #getHighSpeedVideoFpsRangesFor(Size)
      * @see CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO
@@ -836,6 +840,10 @@
      * supported for the same recording rate.</li>
      * </p>
      *
+     * <p>This function returns an empty array if
+     * {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO}
+     * is not supported.</p>
+     *
      * @return an array of supported high speed video recording FPS ranges The upper bound of
      *         returned ranges is guaranteed to be larger or equal to 120.
      * @see #getHighSpeedVideoSizesFor
diff --git a/core/java/android/view/accessibility/a11ychecker/Android.bp b/core/java/android/view/accessibility/a11ychecker/Android.bp
deleted file mode 100644
index e5a577c..0000000
--- a/core/java/android/view/accessibility/a11ychecker/Android.bp
+++ /dev/null
@@ -1,7 +0,0 @@
-java_library_static {
-    name: "A11yChecker",
-    srcs: [
-        "*.java",
-    ],
-    visibility: ["//visibility:public"],
-}
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index d277169..41696df 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -63,7 +63,6 @@
         "-c fa",
     ],
     static_libs: [
-        "A11yChecker",
         "collector-device-lib-platform",
         "frameworks-base-testutils",
         "core-test-rules", // for libcore.dalvik.system.CloseGuardSupport
diff --git a/core/tests/coretests/src/android/view/accessibility/a11ychecker/OWNERS b/core/tests/coretests/src/android/view/accessibility/a11ychecker/OWNERS
deleted file mode 100644
index 872a180..0000000
--- a/core/tests/coretests/src/android/view/accessibility/a11ychecker/OWNERS
+++ /dev/null
@@ -1,5 +0,0 @@
-# Android Accessibility Framework owners
-include /core/java/android/view/accessibility/a11ychecker/OWNERS
-include /services/accessibility/OWNERS
-
-yaraabdullatif@google.com
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 ff40d4c..8d63ff2 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
@@ -1980,12 +1980,6 @@
             }
             clearContentOverlay();
         }
-        if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) {
-            // Avoid double removal, which is fatal.
-            ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                    "%s: trying to remove overlay (%s) while in UNDEFINED state", TAG, surface);
-            return;
-        }
         if (surface == null || !surface.isValid()) {
             ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                     "%s: trying to remove invalid content overlay (%s)", TAG, surface);
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index d42b256..d20b7f0 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -762,14 +762,14 @@
 
     void unregisterLoudnessCodecUpdatesDispatcher(in ILoudnessCodecUpdatesDispatcher dispatcher);
 
-    oneway void startLoudnessCodecUpdates(int sessionId);
+    void startLoudnessCodecUpdates(int sessionId);
 
-    oneway void stopLoudnessCodecUpdates(int sessionId);
+    void stopLoudnessCodecUpdates(int sessionId);
 
-    oneway void addLoudnessCodecInfo(int sessionId, int mediaCodecHash,
+    void addLoudnessCodecInfo(int sessionId, int mediaCodecHash,
             in LoudnessCodecInfo codecInfo);
 
-    oneway void removeLoudnessCodecInfo(int sessionId, in LoudnessCodecInfo codecInfo);
+    void removeLoudnessCodecInfo(int sessionId, in LoudnessCodecInfo codecInfo);
 
     PersistableBundle getLoudnessParams(in LoudnessCodecInfo codecInfo);
 
diff --git a/nfc/java/android/nfc/AvailableNfcAntenna.java b/nfc/java/android/nfc/AvailableNfcAntenna.java
index 6e6512a..e76aeb0 100644
--- a/nfc/java/android/nfc/AvailableNfcAntenna.java
+++ b/nfc/java/android/nfc/AvailableNfcAntenna.java
@@ -28,13 +28,13 @@
 public final class AvailableNfcAntenna implements Parcelable {
     /**
      * Location of the antenna on the Y axis in millimeters.
-     * 0 is the bottom-left when the user is facing the screen
+     * 0 is the top-left when the user is facing the screen
      * and the device orientation is Portrait.
      */
     private final int mLocationX;
     /**
      * Location of the antenna on the Y axis in millimeters.
-     * 0 is the bottom-left when the user is facing the screen
+     * 0 is the top-left when the user is facing the screen
      * and the device orientation is Portrait.
      */
     private final int mLocationY;
@@ -46,7 +46,7 @@
 
     /**
      * Location of the antenna on the X axis in millimeters.
-     * 0 is the bottom-left when the user is facing the screen
+     * 0 is the top-left when the user is facing the screen
      * and the device orientation is Portrait.
      */
     public int getLocationX() {
@@ -55,7 +55,7 @@
 
     /**
      * Location of the antenna on the Y axis in millimeters.
-     * 0 is the bottom-left when the user is facing the screen
+     * 0 is the top-left when the user is facing the screen
      * and the device orientation is Portrait.
      */
     public int getLocationY() {
diff --git a/nfc/java/android/nfc/NfcAntennaInfo.java b/nfc/java/android/nfc/NfcAntennaInfo.java
index b002ca2..c57b2e0 100644
--- a/nfc/java/android/nfc/NfcAntennaInfo.java
+++ b/nfc/java/android/nfc/NfcAntennaInfo.java
@@ -64,9 +64,9 @@
 
     /**
      * Whether the device is foldable. When the device is foldable,
-     * the 0, 0 is considered to be bottom-left when the device is unfolded and
+     * the 0, 0 is considered to be top-left when the device is unfolded and
      * the screens are facing the user. For non-foldable devices 0, 0
-     * is bottom-left when the user is facing the screen.
+     * is top-left when the user is facing the screen.
      */
     public boolean isDeviceFoldable() {
         return mDeviceFoldable;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
index 418cecd..4f5d0e5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
+import com.android.systemui.biometrics.FaceHelpMessageDebouncer
 import com.android.systemui.biometrics.data.repository.FaceSensorInfo
 import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository
 import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
@@ -75,11 +76,16 @@
     private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
     private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
     private lateinit var underTest: BouncerMessageViewModel
+    private val ignoreHelpMessageId = 1
 
     @Before
     fun setUp() {
         kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER))
         kosmos.fakeComposeBouncerFlags.composeBouncerEnabled = true
+        overrideResource(
+            R.array.config_face_acquire_device_entry_ignorelist,
+            intArrayOf(ignoreHelpMessageId)
+        )
         underTest = kosmos.bouncerMessageViewModel
         overrideResource(R.string.kg_trust_agent_disabled, "Trust agent is unavailable")
         kosmos.fakeSystemPropertiesHelper.set(
@@ -379,7 +385,15 @@
             runCurrent()
 
             kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
-                HelpFaceAuthenticationStatus(1, "some helpful message")
+                HelpFaceAuthenticationStatus(0, "some helpful message", 0)
+            )
+            runCurrent()
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                HelpFaceAuthenticationStatus(
+                    0,
+                    "some helpful message",
+                    FaceHelpMessageDebouncer.DEFAULT_WINDOW_MS
+                )
             )
             runCurrent()
             assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index 2546f27..2bf50b3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
@@ -52,7 +52,6 @@
 import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus
 import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus
 import com.android.systemui.deviceentry.shared.model.FaceDetectionStatus
-import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus
 import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus
 import com.android.systemui.display.data.repository.displayRepository
 import com.android.systemui.dump.DumpManager
@@ -79,7 +78,6 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.powerInteractor
-import com.android.systemui.res.R
 import com.android.systemui.statusbar.commandQueue
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.testKosmos
@@ -478,29 +476,6 @@
         }
 
     @Test
-    fun faceHelpMessagesAreIgnoredBasedOnConfig() =
-        testScope.runTest {
-            overrideResource(
-                R.array.config_face_acquire_device_entry_ignorelist,
-                intArrayOf(10, 11)
-            )
-            underTest = createDeviceEntryFaceAuthRepositoryImpl()
-            initCollectors()
-            allPreconditionsToRunFaceAuthAreTrue()
-
-            underTest.requestAuthenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
-            faceAuthenticateIsCalled()
-
-            authenticationCallback.value.onAuthenticationHelp(9, "help msg")
-            authenticationCallback.value.onAuthenticationHelp(10, "Ignored help msg")
-            authenticationCallback.value.onAuthenticationHelp(11, "Ignored help msg")
-
-            val response = authStatus() as HelpFaceAuthenticationStatus
-            assertThat(response.msg).isEqualTo("help msg")
-            assertThat(response.msgId).isEqualTo(response.msgId)
-        }
-
-    @Test
     fun dumpDoesNotErrorOutWhenFaceManagerOrBypassControllerIsNull() =
         testScope.runTest {
             fakeUserRepository.setSelectedUserInfo(primaryUser)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDebouncer.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDebouncer.kt
new file mode 100644
index 0000000..1685f49
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDebouncer.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.util.Log
+import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus
+
+/**
+ * Debounces face help messages with parameters:
+ * - window: Window of time (in milliseconds) to analyze face acquired messages)
+ * - startWindow: Window of time on start required before showing the first help message
+ * - shownFaceMessageFrequencyBoost: Frequency boost given to messages that are currently shown to
+ *   the user
+ */
+class FaceHelpMessageDebouncer(
+    private val window: Long = DEFAULT_WINDOW_MS,
+    private val startWindow: Long = window,
+    private val shownFaceMessageFrequencyBoost: Int = 4,
+) {
+    private val TAG = "FaceHelpMessageDebouncer"
+    private var startTime = 0L
+    private var helpFaceAuthStatuses: MutableList<HelpFaceAuthenticationStatus> = mutableListOf()
+    private var lastMessageIdShown: Int? = null
+
+    /** Remove messages that are outside of the time [window]. */
+    private fun removeOldMessages(currTimestamp: Long) {
+        var numToRemove = 0
+        // This works under the assumption that timestamps are ordered from first to last
+        // in chronological order
+        for (index in helpFaceAuthStatuses.indices) {
+            if ((helpFaceAuthStatuses[index].createdAt + window) >= currTimestamp) {
+                break // all timestamps from here and on are within the window
+            }
+            numToRemove += 1
+        }
+
+        // Remove all outside time window
+        repeat(numToRemove) { helpFaceAuthStatuses.removeFirst() }
+
+        if (numToRemove > 0) {
+            Log.v(TAG, "removedFirst=$numToRemove")
+        }
+    }
+
+    private fun getMostFrequentHelpMessage(): HelpFaceAuthenticationStatus? {
+        // freqMap: msgId => frequency
+        val freqMap = helpFaceAuthStatuses.groupingBy { it.msgId }.eachCount().toMutableMap()
+
+        // Give shownFaceMessageFrequencyBoost to lastMessageIdShown
+        if (lastMessageIdShown != null) {
+            freqMap.computeIfPresent(lastMessageIdShown!!) { _, value ->
+                value + shownFaceMessageFrequencyBoost
+            }
+        }
+        // Go through all msgId keys & find the highest frequency msgId
+        val msgIdWithHighestFrequency =
+            freqMap.entries
+                .maxWithOrNull { (msgId1, freq1), (msgId2, freq2) ->
+                    // ties are broken by more recent message
+                    if (freq1 == freq2) {
+                        helpFaceAuthStatuses
+                            .findLast { it.msgId == msgId1 }!!
+                            .createdAt
+                            .compareTo(
+                                helpFaceAuthStatuses.findLast { it.msgId == msgId2 }!!.createdAt
+                            )
+                    } else {
+                        freq1.compareTo(freq2)
+                    }
+                }
+                ?.key
+        return helpFaceAuthStatuses.findLast { it.msgId == msgIdWithHighestFrequency }
+    }
+
+    fun addMessage(helpFaceAuthStatus: HelpFaceAuthenticationStatus) {
+        helpFaceAuthStatuses.add(helpFaceAuthStatus)
+        Log.v(TAG, "added message=$helpFaceAuthStatus")
+    }
+
+    fun getMessageToShow(atTimestamp: Long): HelpFaceAuthenticationStatus? {
+        if (helpFaceAuthStatuses.isEmpty() || (atTimestamp - startTime) < startWindow) {
+            // there's not enough time that has passed to determine whether to show anything yet
+            Log.v(TAG, "No message; haven't made initial threshold window OR no messages")
+            return null
+        }
+        removeOldMessages(atTimestamp)
+        val messageToShow = getMostFrequentHelpMessage()
+        if (lastMessageIdShown != messageToShow?.msgId) {
+            Log.v(
+                TAG,
+                "showMessage previousLastMessageId=$lastMessageIdShown" +
+                    "\n\tmessageToShow=$messageToShow " +
+                    "\n\thelpFaceAuthStatusesSize=${helpFaceAuthStatuses.size}" +
+                    "\n\thelpFaceAuthStatuses=$helpFaceAuthStatuses"
+            )
+            lastMessageIdShown = messageToShow?.msgId
+        }
+        return messageToShow
+    }
+
+    fun startNewFaceAuthSession(faceAuthStartedTime: Long) {
+        Log.d(TAG, "startNewFaceAuthSession at startTime=$startTime")
+        startTime = faceAuthStartedTime
+        helpFaceAuthStatuses.clear()
+        lastMessageIdShown = null
+    }
+
+    companion object {
+        const val DEFAULT_WINDOW_MS = 200L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index c868d01..430887d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -94,8 +94,16 @@
 
         val textColorError =
             view.resources.getColor(R.color.biometric_dialog_error, view.context.theme)
+
+        val attributes =
+            view.context.obtainStyledAttributes(
+                R.style.TextAppearance_AuthCredential_Indicator,
+                intArrayOf(android.R.attr.textColor)
+            )
         val textColorHint =
-            view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme)
+            if (constraintBp()) attributes.getColor(0, 0)
+            else view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme)
+        attributes.recycle()
 
         val logoView = view.requireViewById<ImageView>(R.id.logo)
         val logoDescriptionView = view.requireViewById<TextView>(R.id.logo_description)
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt
index fa52dad..9460eaf 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -57,16 +57,13 @@
 import com.android.systemui.log.SessionTracker
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.res.R
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.user.data.model.SelectionStatus
 import com.android.systemui.user.data.repository.UserRepository
 import com.google.errorprone.annotations.CompileTimeConstant
 import java.io.PrintWriter
-import java.util.Arrays
 import java.util.concurrent.Executor
-import java.util.stream.Collectors
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
@@ -170,7 +167,6 @@
 ) : DeviceEntryFaceAuthRepository, Dumpable {
     private var authCancellationSignal: CancellationSignal? = null
     private var detectCancellationSignal: CancellationSignal? = null
-    private var faceAcquiredInfoIgnoreList: Set<Int>
     private var retryCount = 0
 
     private var pendingAuthenticateRequest = MutableStateFlow<AuthenticationRequest?>(null)
@@ -240,14 +236,6 @@
             faceManager?.addLockoutResetCallback(faceLockoutResetCallback)
             faceAuthLogger.addLockoutResetCallbackDone()
         }
-        faceAcquiredInfoIgnoreList =
-            Arrays.stream(
-                    context.resources.getIntArray(
-                        R.array.config_face_acquire_device_entry_ignorelist
-                    )
-                )
-                .boxed()
-                .collect(Collectors.toSet())
         dumpManager.registerCriticalDumpable("DeviceEntryFaceAuthRepositoryImpl", this)
 
         canRunFaceAuth =
@@ -485,10 +473,8 @@
             }
 
             override fun onAuthenticationHelp(code: Int, helpStr: CharSequence?) {
-                if (faceAcquiredInfoIgnoreList.contains(code)) {
-                    return
-                }
-                _authenticationStatus.value = HelpFaceAuthenticationStatus(code, helpStr.toString())
+                _authenticationStatus.value =
+                    HelpFaceAuthenticationStatus(code, helpStr?.toString())
             }
 
             override fun onAuthenticationSucceeded(result: FaceManager.AuthenticationResult) {
@@ -731,7 +717,6 @@
         pw.println("  _pendingAuthenticateRequest: ${pendingAuthenticateRequest.value}")
         pw.println("  authCancellationSignal: $authCancellationSignal")
         pw.println("  detectCancellationSignal: $detectCancellationSignal")
-        pw.println("  faceAcquiredInfoIgnoreList: $faceAcquiredInfoIgnoreList")
         pw.println("  _authenticationStatus: ${_authenticationStatus.value}")
         pw.println("  _detectionStatus: ${_detectionStatus.value}")
         pw.println("  currentUserId: $currentUserId")
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractor.kt
new file mode 100644
index 0000000..34b1544
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractor.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.deviceentry.domain.interactor
+
+import android.content.res.Resources
+import android.hardware.biometrics.BiometricFaceConstants
+import com.android.systemui.biometrics.FaceHelpMessageDebouncer
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository
+import com.android.systemui.deviceentry.shared.model.AcquiredFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus
+import com.android.systemui.res.R
+import java.util.Arrays
+import java.util.stream.Collectors
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transform
+
+/**
+ * Process face authentication statuses.
+ * - Ignores face help messages based on R.array.config_face_acquire_device_entry_ignorelist.
+ * - Uses FaceHelpMessageDebouncer to debounce flickery help messages.
+ */
+@SysUISingleton
+class DeviceEntryFaceAuthStatusInteractor
+@Inject
+constructor(
+    repository: DeviceEntryFaceAuthRepository,
+    @Main private val resources: Resources,
+    @Application private val applicationScope: CoroutineScope,
+) {
+    private val faceHelpMessageDebouncer = FaceHelpMessageDebouncer()
+    private var faceAcquiredInfoIgnoreList: Set<Int> =
+        Arrays.stream(resources.getIntArray(R.array.config_face_acquire_device_entry_ignorelist))
+            .boxed()
+            .collect(Collectors.toSet())
+
+    val authenticationStatus: StateFlow<FaceAuthenticationStatus?> =
+        repository.authenticationStatus
+            .transform { authenticationStatus ->
+                if (authenticationStatus is AcquiredFaceAuthenticationStatus) {
+                    if (
+                        authenticationStatus.acquiredInfo ==
+                            BiometricFaceConstants.FACE_ACQUIRED_START
+                    ) {
+                        faceHelpMessageDebouncer.startNewFaceAuthSession(
+                            authenticationStatus.createdAt
+                        )
+                    }
+                }
+
+                if (authenticationStatus is HelpFaceAuthenticationStatus) {
+                    if (!faceAcquiredInfoIgnoreList.contains(authenticationStatus.msgId)) {
+                        faceHelpMessageDebouncer.addMessage(authenticationStatus)
+                    }
+
+                    val messageToShow =
+                        faceHelpMessageDebouncer.getMessageToShow(
+                            atTimestamp = authenticationStatus.createdAt,
+                        )
+                    if (messageToShow != null) {
+                        emit(messageToShow)
+                    }
+
+                    return@transform
+                }
+
+                emit(authenticationStatus)
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = null,
+            )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
index d12ea45..c536d6b 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
@@ -90,6 +90,7 @@
     private val powerInteractor: PowerInteractor,
     private val biometricSettingsRepository: BiometricSettingsRepository,
     private val trustManager: TrustManager,
+    deviceEntryFaceAuthStatusInteractor: DeviceEntryFaceAuthStatusInteractor,
 ) : DeviceEntryFaceAuthInteractor {
 
     private val listeners: MutableList<FaceAuthenticationListener> = mutableListOf()
@@ -276,9 +277,13 @@
     }
 
     private val faceAuthenticationStatusOverride = MutableStateFlow<FaceAuthenticationStatus?>(null)
+
     /** Provide the status of face authentication */
     override val authenticationStatus =
-        merge(faceAuthenticationStatusOverride.filterNotNull(), repository.authenticationStatus)
+        merge(
+            faceAuthenticationStatusOverride.filterNotNull(),
+            deviceEntryFaceAuthStatusInteractor.authenticationStatus.filterNotNull(),
+        )
 
     /** Provide the status of face detection */
     override val detectionStatus = repository.detectionStatus
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt
new file mode 100644
index 0000000..baef620
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.hardware.biometrics.BiometricFaceConstants
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@TestableLooper.RunWithLooper
+class FaceHelpMessageDebouncerTest : SysuiTestCase() {
+    private lateinit var underTest: FaceHelpMessageDebouncer
+    private val window = 9L
+    private val startWindow = 4L
+    private val shownFaceMessageFrequencyBoost = 2
+
+    @Before
+    fun setUp() {
+        underTest =
+            FaceHelpMessageDebouncer(
+                window = window,
+                startWindow = startWindow,
+                shownFaceMessageFrequencyBoost = shownFaceMessageFrequencyBoost,
+            )
+    }
+
+    @Test
+    fun getMessageBeforeStartWindow_null() {
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                "testTooClose",
+                0
+            )
+        )
+        assertThat(underTest.getMessageToShow(0)).isNull()
+    }
+
+    @Test
+    fun getMessageAfterStartWindow() {
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                "tooClose",
+                0
+            )
+        )
+
+        assertThat(underTest.getMessageToShow(startWindow)?.msgId)
+            .isEqualTo(BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE)
+        assertThat(underTest.getMessageToShow(startWindow)?.msg).isEqualTo("tooClose")
+    }
+
+    @Test
+    fun getMessageAfterMessagesCleared_null() {
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                "tooClose",
+                0
+            )
+        )
+        underTest.startNewFaceAuthSession(0)
+
+        assertThat(underTest.getMessageToShow(startWindow)).isNull()
+    }
+
+    @Test
+    fun messagesBeforeWindowRemoved() {
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                "tooClose",
+                0
+            )
+        )
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                "tooClose",
+                0
+            )
+        )
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                "tooClose",
+                window - 1
+            )
+        )
+        val lastMessage =
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT,
+                "tooBright",
+                window
+            )
+        underTest.addMessage(lastMessage)
+
+        assertThat(underTest.getMessageToShow(window + 1)).isEqualTo(lastMessage)
+    }
+
+    @Test
+    fun getMessageTieGoesToMostRecent() {
+        for (i in 1..window step 2) {
+            underTest.addMessage(
+                HelpFaceAuthenticationStatus(
+                    BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                    "tooClose",
+                    i
+                )
+            )
+            underTest.addMessage(
+                HelpFaceAuthenticationStatus(
+                    BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT,
+                    "tooBright",
+                    i + 1
+                )
+            )
+        }
+
+        assertThat(underTest.getMessageToShow(window)?.msgId)
+            .isEqualTo(BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT)
+        assertThat(underTest.getMessageToShow(window)?.msg).isEqualTo("tooBright")
+    }
+
+    @Test
+    fun boostCurrentlyShowingMessage() {
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT,
+                "tooBright",
+                0
+            )
+        )
+
+        val lastMessageShown = underTest.getMessageToShow(startWindow)
+        assertThat(lastMessageShown?.msgId)
+            .isEqualTo(BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT)
+
+        for (i in 1..<shownFaceMessageFrequencyBoost) {
+            underTest.addMessage(
+                HelpFaceAuthenticationStatus(
+                    BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                    "tooClose",
+                    startWindow
+                )
+            )
+        }
+
+        // although technically there's a different msgId with a higher frequency count now, the
+        // shownFaceMessageFrequencyBoost causes the last message shown to get a "boost"
+        // to keep showing
+        assertThat(underTest.getMessageToShow(startWindow)).isEqualTo(lastMessageShown)
+    }
+
+    @Test
+    fun overcomeBoostedCurrentlyShowingMessage() {
+        // Comments are assuming shownFaceMessageFrequencyBoost = 2
+        // [B], weights: B=1
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT,
+                "tooBright",
+                0
+            )
+        )
+
+        // [B], showing messageB, weights: B=3
+        val messageB = underTest.getMessageToShow(startWindow)
+
+        // [B, C, C], showing messageB, weights: B=3, C=2
+        for (i in 1..shownFaceMessageFrequencyBoost) {
+            underTest.addMessage(
+                HelpFaceAuthenticationStatus(
+                    BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                    "tooClose",
+                    startWindow
+                )
+            )
+        }
+        // messageB is getting boosted to continue to show
+        assertThat(underTest.getMessageToShow(startWindow)).isEqualTo(messageB)
+
+        // receive one more FACE_ACQUIRED_TOO_CLOSE acquired info to pass the boost
+        // [C, C, C], showing messageB, weights: B=2, C=3
+        underTest.addMessage(
+            HelpFaceAuthenticationStatus(
+                BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE,
+                "tooClose",
+                startWindow
+            )
+        )
+
+        // Now FACE_ACQUIRED_TOO_CLOSE has surpassed the boosted messageB frequency
+        // [C, C, C], showing messageC, weights: C=5
+        assertThat(underTest.getMessageToShow(startWindow)?.msgId)
+            .isEqualTo(BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorTest.kt
index 431fef6..6fd8660 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/BiometricMessageInteractorTest.kt
@@ -24,6 +24,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.FaceHelpMessageDebouncer
 import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
 import com.android.systemui.biometrics.domain.faceHelpMessageDeferral
 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
@@ -45,6 +46,7 @@
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -264,11 +266,20 @@
             biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
             biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
 
-            // WHEN authentication status help
+            // WHEN authentication status help past debouncer
             faceAuthRepository.setAuthenticationStatus(
                 HelpFaceAuthenticationStatus(
                     msg = "Move left",
                     msgId = FACE_ACQUIRED_TOO_RIGHT,
+                    createdAt = 0L,
+                )
+            )
+            runCurrent()
+            faceAuthRepository.setAuthenticationStatus(
+                HelpFaceAuthenticationStatus(
+                    msg = "Move left",
+                    msgId = FACE_ACQUIRED_TOO_RIGHT,
+                    createdAt = FaceHelpMessageDebouncer.DEFAULT_WINDOW_MS,
                 )
             )
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
index 529cd6e..0b7a3ed 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
@@ -90,6 +90,7 @@
     private val keyguardUpdateMonitor = kosmos.keyguardUpdateMonitor
     private val faceWakeUpTriggersConfig = kosmos.fakeFaceWakeUpTriggersConfig
     private val trustManager = kosmos.trustManager
+    private val deviceEntryFaceAuthStatusInteractor = kosmos.deviceEntryFaceAuthStatusInteractor
 
     @Before
     fun setup() {
@@ -112,6 +113,7 @@
                 powerInteractor,
                 fakeBiometricSettingsRepository,
                 trustManager,
+                deviceEntryFaceAuthStatusInteractor,
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorTest.kt
new file mode 100644
index 0000000..6022d9c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorTest.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.deviceentry.domain.interactor
+
+import android.hardware.biometrics.BiometricFaceConstants
+import android.hardware.face.FaceManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.FaceHelpMessageDebouncer.Companion.DEFAULT_WINDOW_MS
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.shared.model.AcquiredFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DeviceEntryFaceAuthStatusInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope: TestScope = kosmos.testScope
+    private lateinit var underTest: DeviceEntryFaceAuthStatusInteractor
+    private val ignoreHelpMessageId = 1
+
+    @Before
+    fun setup() {
+        overrideResource(
+            R.array.config_face_acquire_device_entry_ignorelist,
+            intArrayOf(ignoreHelpMessageId)
+        )
+        underTest = kosmos.deviceEntryFaceAuthStatusInteractor
+    }
+
+    @Test
+    fun successAuthenticationStatus() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            val successStatus =
+                SuccessFaceAuthenticationStatus(
+                    successResult = mock(FaceManager.AuthenticationResult::class.java)
+                )
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(successStatus)
+            assertThat(authenticationStatus).isEqualTo(successStatus)
+        }
+
+    @Test
+    fun acquiredFaceAuthenticationStatus() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            val acquiredStatus = AcquiredFaceAuthenticationStatus(acquiredInfo = 0)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(acquiredStatus)
+            assertThat(authenticationStatus).isEqualTo(acquiredStatus)
+        }
+
+    @Test
+    fun failedFaceAuthenticationStatus() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            val failedStatus = FailedFaceAuthenticationStatus()
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(failedStatus)
+            assertThat(authenticationStatus).isEqualTo(failedStatus)
+        }
+
+    @Test
+    fun errorFaceAuthenticationStatus() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            val errorStatus = ErrorFaceAuthenticationStatus(0, "test")
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(errorStatus)
+            assertThat(authenticationStatus).isEqualTo(errorStatus)
+        }
+
+    @Test
+    fun firstHelpFaceAuthenticationStatus_noUpdate() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                AcquiredFaceAuthenticationStatus(
+                    BiometricFaceConstants.FACE_ACQUIRED_START,
+                    createdAt = 0
+                )
+            )
+            val helpMessage = HelpFaceAuthenticationStatus(0, "test", 1)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(helpMessage)
+            assertThat(authenticationStatus).isNull()
+        }
+
+    @Test
+    fun helpFaceAuthenticationStatus_afterWindow() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                HelpFaceAuthenticationStatus(0, "test1", 0)
+            )
+            runCurrent()
+            val helpMessage = HelpFaceAuthenticationStatus(0, "test2", DEFAULT_WINDOW_MS)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(helpMessage)
+            runCurrent()
+            assertThat(authenticationStatus).isEqualTo(helpMessage)
+        }
+
+    @Test
+    fun helpFaceAuthenticationStatus_onlyIgnoredHelpMessages_afterWindow() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                HelpFaceAuthenticationStatus(ignoreHelpMessageId, "ignoredMsg", 0)
+            )
+            runCurrent()
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                HelpFaceAuthenticationStatus(ignoreHelpMessageId, "ignoredMsg", DEFAULT_WINDOW_MS)
+            )
+            runCurrent()
+            assertThat(authenticationStatus).isNull()
+        }
+
+    @Test
+    fun helpFaceAuthenticationStatus_afterWindow_onIgnoredMessage_showsOtherMessageInstead() =
+        testScope.runTest {
+            val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+            val validHelpMessage = HelpFaceAuthenticationStatus(0, "validHelpMsg", 0)
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(validHelpMessage)
+            runCurrent()
+            // help message that should be ignored
+            kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+                HelpFaceAuthenticationStatus(ignoreHelpMessageId, "ignoredMsg", DEFAULT_WINDOW_MS)
+            )
+            runCurrent()
+            assertThat(authenticationStatus).isEqualTo(validHelpMessage)
+        }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
index a8fc27a..b9be04d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
@@ -57,5 +57,6 @@
             powerInteractor = powerInteractor,
             biometricSettingsRepository = biometricSettingsRepository,
             trustManager = trustManager,
+            deviceEntryFaceAuthStatusInteractor = deviceEntryFaceAuthStatusInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorKosmos.kt
new file mode 100644
index 0000000..66d3709
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthStatusInteractorKosmos.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.deviceentry.domain.interactor
+
+import android.content.res.mainResources
+import com.android.systemui.keyguard.data.repository.deviceEntryFaceAuthRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.deviceEntryFaceAuthStatusInteractor by
+    Kosmos.Fixture {
+        DeviceEntryFaceAuthStatusInteractor(
+            repository = deviceEntryFaceAuthRepository,
+            resources = mainResources,
+            applicationScope = applicationCoroutineScope,
+        )
+    }
diff --git a/services/accessibility/Android.bp b/services/accessibility/Android.bp
index 7a99b60..311addb 100644
--- a/services/accessibility/Android.bp
+++ b/services/accessibility/Android.bp
@@ -29,10 +29,12 @@
         "//frameworks/base/packages/SettingsLib/RestrictedLockUtils:SettingsLibRestrictedLockUtilsSrc",
     ],
     libs: [
+        "aatf",
         "services.core",
         "androidx.annotation_annotation",
     ],
     static_libs: [
+        "a11ychecker-protos-java-proto-lite",
         "com_android_server_accessibility_flags_lib",
         "//frameworks/base/packages/SystemUI/aconfig:com_android_systemui_flags_lib",
 
@@ -68,3 +70,14 @@
     name: "com_android_server_accessibility_flags_lib",
     aconfig_declarations: "com_android_server_accessibility_flags",
 }
+
+java_library_static {
+    name: "a11ychecker-protos-java-proto-lite",
+    proto: {
+        type: "lite",
+        canonical_path_from_root: false,
+    },
+    srcs: [
+        "java/**/a11ychecker/proto/*.proto",
+    ],
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java
new file mode 100644
index 0000000..55af9a0
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.a11ychecker;
+
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.util.Slog;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckClass;
+import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultReported;
+import com.android.server.accessibility.a11ychecker.A11yCheckerProto.AccessibilityCheckResultType;
+
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult;
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck;
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult;
+import com.google.android.apps.common.testing.accessibility.framework.checks.ClassNameCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.ClickableSpanCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.DuplicateClickableBoundsCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.DuplicateSpeakableTextCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.EditableContentDescCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.ImageContrastCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.LinkPurposeUnclearCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.RedundantDescriptionCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.TextContrastCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.TextSizeCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.TraversalOrderCheck;
+
+import java.util.AbstractMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Util class to process a11y checker results for logging.
+ *
+ * @hide
+ */
+public class AccessibilityCheckerUtils {
+
+    private static final String LOG_TAG = "AccessibilityCheckerUtils";
+    @VisibleForTesting
+    // LINT.IfChange
+    static final Map<Class<? extends AccessibilityHierarchyCheck>, AccessibilityCheckClass>
+            CHECK_CLASS_TO_ENUM_MAP =
+            Map.ofEntries(
+                    classMapEntry(ClassNameCheck.class, AccessibilityCheckClass.CLASS_NAME_CHECK),
+                    classMapEntry(ClickableSpanCheck.class,
+                            AccessibilityCheckClass.CLICKABLE_SPAN_CHECK),
+                    classMapEntry(DuplicateClickableBoundsCheck.class,
+                            AccessibilityCheckClass.DUPLICATE_CLICKABLE_BOUNDS_CHECK),
+                    classMapEntry(DuplicateSpeakableTextCheck.class,
+                            AccessibilityCheckClass.DUPLICATE_SPEAKABLE_TEXT_CHECK),
+                    classMapEntry(EditableContentDescCheck.class,
+                            AccessibilityCheckClass.EDITABLE_CONTENT_DESC_CHECK),
+                    classMapEntry(ImageContrastCheck.class,
+                            AccessibilityCheckClass.IMAGE_CONTRAST_CHECK),
+                    classMapEntry(LinkPurposeUnclearCheck.class,
+                            AccessibilityCheckClass.LINK_PURPOSE_UNCLEAR_CHECK),
+                    classMapEntry(RedundantDescriptionCheck.class,
+                            AccessibilityCheckClass.REDUNDANT_DESCRIPTION_CHECK),
+                    classMapEntry(SpeakableTextPresentCheck.class,
+                            AccessibilityCheckClass.SPEAKABLE_TEXT_PRESENT_CHECK),
+                    classMapEntry(TextContrastCheck.class,
+                            AccessibilityCheckClass.TEXT_CONTRAST_CHECK),
+                    classMapEntry(TextSizeCheck.class, AccessibilityCheckClass.TEXT_SIZE_CHECK),
+                    classMapEntry(TouchTargetSizeCheck.class,
+                            AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK),
+                    classMapEntry(TraversalOrderCheck.class,
+                            AccessibilityCheckClass.TRAVERSAL_ORDER_CHECK));
+    // LINT.ThenChange(/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto)
+
+    static Set<AccessibilityCheckResultReported> processResults(
+            Context context,
+            AccessibilityNodeInfo nodeInfo,
+            List<AccessibilityHierarchyCheckResult> checkResults,
+            @Nullable AccessibilityEvent accessibilityEvent,
+            ComponentName a11yServiceComponentName) {
+        return processResults(nodeInfo, checkResults, accessibilityEvent,
+                context.getPackageManager(), a11yServiceComponentName);
+    }
+
+    @VisibleForTesting
+    static Set<AccessibilityCheckResultReported> processResults(
+            AccessibilityNodeInfo nodeInfo,
+            List<AccessibilityHierarchyCheckResult> checkResults,
+            @Nullable AccessibilityEvent accessibilityEvent,
+            PackageManager packageManager,
+            ComponentName a11yServiceComponentName) {
+        String appPackageName = nodeInfo.getPackageName().toString();
+        AccessibilityCheckResultReported.Builder builder;
+        try {
+            builder = AccessibilityCheckResultReported.newBuilder()
+                    .setPackageName(appPackageName)
+                    .setAppVersionCode(getAppVersionCode(packageManager, appPackageName))
+                    .setUiElementPath(AccessibilityNodePathBuilder.createNodePath(nodeInfo))
+                    .setActivityName(getActivityName(packageManager, accessibilityEvent))
+                    .setWindowTitle(getWindowTitle(nodeInfo))
+                    .setSourceComponentName(a11yServiceComponentName.flattenToString())
+                    .setSourceVersionCode(
+                            getAppVersionCode(packageManager,
+                                    a11yServiceComponentName.getPackageName()));
+        } catch (PackageManager.NameNotFoundException e) {
+            Slog.e(LOG_TAG, "Unknown package name", e);
+            return Set.of();
+        }
+
+        return checkResults.stream()
+                .filter(checkResult -> checkResult.getType()
+                        == AccessibilityCheckResult.AccessibilityCheckResultType.ERROR
+                        || checkResult.getType()
+                        == AccessibilityCheckResult.AccessibilityCheckResultType.WARNING)
+                .map(checkResult -> builder.setResultCheckClass(
+                        getCheckClass(checkResult)).setResultType(
+                        getCheckResultType(checkResult)).setResultId(
+                        checkResult.getResultId()).build())
+                .collect(Collectors.toUnmodifiableSet());
+    }
+
+    private static long getAppVersionCode(PackageManager packageManager, String packageName) throws
+            PackageManager.NameNotFoundException {
+        PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
+        return packageInfo.getLongVersionCode();
+    }
+
+    /**
+     * Returns the simple class name of the Activity providing the cache update, if available,
+     * or an empty String if not.
+     */
+    @VisibleForTesting
+    static String getActivityName(
+            PackageManager packageManager, @Nullable AccessibilityEvent accessibilityEvent) {
+        if (accessibilityEvent == null) {
+            return "";
+        }
+        CharSequence activityName = accessibilityEvent.getClassName();
+        if (accessibilityEvent.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
+                && accessibilityEvent.getPackageName() != null
+                && activityName != null) {
+            try {
+                // Check class is for a valid Activity.
+                packageManager
+                        .getActivityInfo(
+                                new ComponentName(accessibilityEvent.getPackageName().toString(),
+                                        activityName.toString()), 0);
+                int qualifierEnd = activityName.toString().lastIndexOf('.');
+                return activityName.toString().substring(qualifierEnd + 1);
+            } catch (PackageManager.NameNotFoundException e) {
+                // No need to spam the logs. This is very frequent when the class doesn't match
+                // an activity.
+            }
+        }
+        return "";
+    }
+
+    /**
+     * Returns the title of the window containing the a11y node.
+     */
+    private static String getWindowTitle(AccessibilityNodeInfo nodeInfo) {
+        if (nodeInfo.getWindow() == null) {
+            return "";
+        }
+        CharSequence windowTitle = nodeInfo.getWindow().getTitle();
+        return windowTitle == null ? "" : windowTitle.toString();
+    }
+
+    /**
+     * Maps the {@link AccessibilityHierarchyCheck} class that produced the given result, with the
+     * corresponding {@link AccessibilityCheckClass} enum. This enumeration is to avoid relying on
+     * String class names in the logging, which can be proguarded. It also reduces the logging size.
+     */
+    private static AccessibilityCheckClass getCheckClass(
+            AccessibilityHierarchyCheckResult checkResult) {
+        if (CHECK_CLASS_TO_ENUM_MAP.containsKey(checkResult.getSourceCheckClass())) {
+            return CHECK_CLASS_TO_ENUM_MAP.get(checkResult.getSourceCheckClass());
+        }
+        return AccessibilityCheckClass.UNKNOWN_CHECK;
+    }
+
+    private static AccessibilityCheckResultType getCheckResultType(
+            AccessibilityHierarchyCheckResult checkResult) {
+        return switch (checkResult.getType()) {
+            case ERROR -> AccessibilityCheckResultType.ERROR;
+            case WARNING -> AccessibilityCheckResultType.WARNING;
+            default -> AccessibilityCheckResultType.UNKNOWN_RESULT_TYPE;
+        };
+    }
+
+    private static Map.Entry<Class<? extends AccessibilityHierarchyCheck>,
+            AccessibilityCheckClass> classMapEntry(
+            Class<? extends AccessibilityHierarchyCheck> checkClass,
+            AccessibilityCheckClass checkClassEnum) {
+        return new AbstractMap.SimpleImmutableEntry<>(checkClass, checkClassEnum);
+    }
+}
diff --git a/core/java/android/view/accessibility/a11ychecker/AccessibilityNodePathBuilder.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilder.java
similarity index 98%
rename from core/java/android/view/accessibility/a11ychecker/AccessibilityNodePathBuilder.java
rename to services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilder.java
index 2996dde..bbfb217 100644
--- a/core/java/android/view/accessibility/a11ychecker/AccessibilityNodePathBuilder.java
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilder.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.view.accessibility.a11ychecker;
+package com.android.server.accessibility.a11ychecker;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
diff --git a/core/java/android/view/accessibility/a11ychecker/OWNERS b/services/accessibility/java/com/android/server/accessibility/a11ychecker/OWNERS
similarity index 100%
rename from core/java/android/view/accessibility/a11ychecker/OWNERS
rename to services/accessibility/java/com/android/server/accessibility/a11ychecker/OWNERS
diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto b/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto
new file mode 100644
index 0000000..8beed4a
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+syntax = "proto2";
+package android.accessibility;
+
+option java_package = "com.android.server.accessibility.a11ychecker";
+option java_outer_classname = "A11yCheckerProto";
+
+// TODO(b/326385939): remove and replace usage with the atom extension proto, when submitted.
+/** Logs the result of an AccessibilityCheck. */
+message AccessibilityCheckResultReported {
+  // Package name of the app containing the checked View.
+  optional string package_name = 1;
+  // Version code of the app containing the checked View.
+  optional int64 app_version_code = 2;
+  // The path of the View starting from the root element in the window. Each element is
+  // represented by the View's resource id, when available, or the View's class name.
+  optional string ui_element_path = 3;
+  // Class name of the activity containing the checked View.
+  optional string activity_name = 4;
+  // Title of the window containing the checked View.
+  optional string window_title = 5;
+  // The flattened component name of the app running the AccessibilityService which provided the a11y node.
+  optional string source_component_name = 6;
+  // Version code of the app running the AccessibilityService that provided the a11y node.
+  optional int64 source_version_code = 7;
+  // Class Name of the AccessibilityCheck that produced the result.
+  optional AccessibilityCheckClass result_check_class = 8;
+  // Result type of the AccessibilityCheckResult.
+  optional AccessibilityCheckResultType result_type = 9;
+  // Result ID of the AccessibilityCheckResult.
+  optional int32 result_id = 10;
+}
+
+/** The AccessibilityCheck class. */
+// LINT.IfChange
+enum AccessibilityCheckClass {
+  UNKNOWN_CHECK = 0;
+  CLASS_NAME_CHECK = 1;
+  CLICKABLE_SPAN_CHECK = 2;
+  DUPLICATE_CLICKABLE_BOUNDS_CHECK = 3;
+  DUPLICATE_SPEAKABLE_TEXT_CHECK = 4;
+  EDITABLE_CONTENT_DESC_CHECK = 5;
+  IMAGE_CONTRAST_CHECK = 6;
+  LINK_PURPOSE_UNCLEAR_CHECK = 7;
+  REDUNDANT_DESCRIPTION_CHECK = 8;
+  SPEAKABLE_TEXT_PRESENT_CHECK = 9;
+  TEXT_CONTRAST_CHECK = 10;
+  TEXT_SIZE_CHECK = 11;
+  TOUCH_TARGET_SIZE_CHECK = 12;
+  TRAVERSAL_ORDER_CHECK = 13;
+}
+// LINT.ThenChange(/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java)
+
+/** The type of AccessibilityCheckResult */
+enum AccessibilityCheckResultType {
+  UNKNOWN_RESULT_TYPE = 0;
+  ERROR = 1;
+  WARNING = 2;
+}
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index d3b41b8..e9ecfc6 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -25,6 +25,7 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.devicestate.DeviceState;
+import android.hardware.devicestate.DeviceStateManager;
 import android.hardware.devicestate.feature.flags.FeatureFlags;
 import android.hardware.devicestate.feature.flags.FeatureFlagsImpl;
 import android.os.Handler;
@@ -614,7 +615,11 @@
                     && isBootCompleted
                     && !mFoldSettingProvider.shouldStayAwakeOnFold();
         } else {
-            return mDeviceStatesOnWhichToSelectiveSleep.get(pendingState.getIdentifier())
+            return currentState.getIdentifier()
+                    != DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER
+                    && pendingState.getIdentifier()
+                    != DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER
+                    && mDeviceStatesOnWhichToSelectiveSleep.get(pendingState.getIdentifier())
                     && !mDeviceStatesOnWhichToSelectiveSleep.get(currentState.getIdentifier())
                     && isInteractive
                     && isBootCompleted
diff --git a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
index 5ff421a..7c93c8b 100644
--- a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
+++ b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
@@ -89,8 +89,8 @@
     void performShowIme(IBinder showInputToken, @NonNull ImeTracker.Token statsToken,
             @InputMethod.ShowFlags int showFlags, ResultReceiver resultReceiver,
             @SoftInputShowHideReason int reason, @UserIdInt int userId) {
-        final var bindingController = mService.getInputMethodBindingController(userId);
         final var userData = mService.getUserData(userId);
+        final var bindingController = userData.mBindingController;
         final IInputMethodInvoker curMethod = bindingController.getCurMethod();
         if (curMethod != null) {
             if (DEBUG) {
@@ -128,9 +128,9 @@
     void performHideIme(IBinder hideInputToken, @NonNull ImeTracker.Token statsToken,
             ResultReceiver resultReceiver, @SoftInputShowHideReason int reason,
             @UserIdInt int userId) {
-        final var bindingController = mService.getInputMethodBindingController(userId);
-        final IInputMethodInvoker curMethod = bindingController.getCurMethod();
         final var userData = mService.getUserData(userId);
+        final var bindingController = userData.mBindingController;
+        final IInputMethodInvoker curMethod = bindingController.getCurMethod();
         if (curMethod != null) {
             // The IME will report its visible state again after the following message finally
             // delivered to the IME process as an IPC.  Hence the inconsistency between
@@ -171,8 +171,8 @@
     void applyImeVisibility(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
             @ImeVisibilityStateComputer.VisibilityState int state,
             @SoftInputShowHideReason int reason, @UserIdInt int userId) {
-        final var bindingController = mService.getInputMethodBindingController(userId);
         final var userData = mService.getUserData(userId);
+        final var bindingController = userData.mBindingController;
         final int displayIdToShowIme = bindingController.getDisplayIdToShowIme();
         switch (state) {
             case STATE_SHOW_IME:
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index e8ad327..540c21c 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1726,7 +1726,7 @@
                     SoftInputShowHideReason.HIDE_REMOVE_CLIENT, userId);
             if (userData.mBoundToMethod) {
                 userData.mBoundToMethod = false;
-                final var userBindingController = getInputMethodBindingController(userId);
+                final var userBindingController = userData.mBindingController;
                 IInputMethodInvoker curMethod = userBindingController.getCurMethod();
                 if (curMethod != null) {
                     // When we unbind input, we are unbinding the client, so we always
@@ -1758,7 +1758,7 @@
                 Slog.v(TAG, "unbindCurrentInputLocked: client="
                         + userData.mCurClient.mClient.asBinder());
             }
-            final var bindingController = getInputMethodBindingController(userId);
+            final var bindingController = userData.mBindingController;
             if (userData.mBoundToMethod) {
                 userData.mBoundToMethod = false;
                 IInputMethodInvoker curMethod = bindingController.getCurMethod();
@@ -1844,8 +1844,8 @@
     @NonNull
     InputBindResult attachNewInputLocked(@StartInputReason int startInputReason, boolean initial,
             @UserIdInt int userId) {
-        final var bindingController = getInputMethodBindingController(userId);
         final var userData = getUserData(userId);
+        final var bindingController = userData.mBindingController;
         if (!userData.mBoundToMethod) {
             bindingController.getCurMethod().bindInput(userData.mCurClient.mBinding);
             userData.mBoundToMethod = true;
@@ -2316,8 +2316,8 @@
                     channel.dispose();
                     return;
                 }
-                final var bindingController = getInputMethodBindingController(userId);
                 final var userData = getUserData(userId);
+                final var bindingController = userData.mBindingController;
                 IInputMethodInvoker curMethod = bindingController.getCurMethod();
                 if (curMethod != null && method != null
                         && curMethod.asBinder() == method.asBinder()) {
@@ -2745,8 +2745,8 @@
 
     @GuardedBy("ImfLock.class")
     private void updateSystemUiLocked(int vis, int backDisposition, @UserIdInt int userId) {
-        final var bindingController = getInputMethodBindingController(userId);
         final var userData = getUserData(userId);
+        final var bindingController = userData.mBindingController;
         final var curToken = bindingController.getCurToken();
         if (curToken == null) {
             return;
@@ -2844,11 +2844,11 @@
                 settings.putSelectedInputMethod(id);
             }
         }
-        final var bindingController = getInputMethodBindingController(userId);
+        final var userData = getUserData(userId);
+        final var bindingController = userData.mBindingController;
         bindingController.setSelectedMethodId(id);
 
         // Also re-initialize controllers.
-        final var userData = getUserData(userId);
         userData.mSwitchingController.resetCircularListLocked(mContext, settings);
         userData.mHardwareKeyboardShortcutController.update(settings);
     }
@@ -2885,7 +2885,8 @@
             }
         }
 
-        final var bindingController = getInputMethodBindingController(userId);
+        final var userData = getUserData(userId);
+        final var bindingController = userData.mBindingController;
         if (bindingController.getDeviceIdToShowIme() == DEVICE_ID_DEFAULT) {
             String ime = SecureSettingsWrapper.getString(
                     Settings.Secure.DEFAULT_INPUT_METHOD, null, userId);
@@ -2925,7 +2926,6 @@
             resetCurrentMethodAndClientLocked(UnbindReason.NO_IME, userId);
         }
 
-        final var userData = getUserData(userId);
         userData.mSwitchingController.resetCircularListLocked(mContext, settings);
         userData.mHardwareKeyboardShortcutController.update(settings);
         sendOnNavButtonFlagsChangedLocked(userData);
@@ -3407,8 +3407,8 @@
         mVisibilityStateComputer.requestImeVisibility(windowToken, true);
 
         // Ensure binding the connection when IME is going to show.
-        final var bindingController = getInputMethodBindingController(userId);
         final var userData = getUserData(userId);
+        final var bindingController = userData.mBindingController;
         bindingController.setCurrentMethodVisible();
         final IInputMethodInvoker curMethod = bindingController.getCurMethod();
         ImeTracker.forLogging().onCancelled(userData.mCurStatsToken,
@@ -3541,7 +3541,8 @@
     boolean hideCurrentInputLocked(IBinder windowToken, @NonNull ImeTracker.Token statsToken,
             @InputMethodManager.HideFlags int flags, @Nullable ResultReceiver resultReceiver,
             @SoftInputShowHideReason int reason, @UserIdInt int userId) {
-        final var bindingController = getInputMethodBindingController(userId);
+        final var userData = getUserData(userId);
+        final var bindingController = userData.mBindingController;
         if (!mVisibilityStateComputer.canHideIme(statsToken, flags)) {
             return false;
         }
@@ -3554,7 +3555,6 @@
         // since Android Eclair.  That's why we need to accept IMM#hideSoftInput() even when only
         // IMMS#InputShown indicates that the software keyboard is shown.
         // TODO(b/246309664): Clean up IMMS#mImeWindowVis
-        final var userData = getUserData(userId);
         IInputMethodInvoker curMethod = bindingController.getCurMethod();
         final boolean shouldHideSoftInput = curMethod != null
                 && (isInputShownLocked()
@@ -3634,6 +3634,7 @@
             Slog.w(TAG, "User #" + userId + " is not running.");
             return InputBindResult.INVALID_USER;
         }
+        final var userData = getUserData(userId);
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
                     "IMMS.startInputOrWindowGainedFocus");
@@ -3641,7 +3642,7 @@
                     "InputMethodManagerService#startInputOrWindowGainedFocus", mDumper);
             final InputBindResult result;
             synchronized (ImfLock.class) {
-                final var bindingController = getInputMethodBindingController(userId);
+                final var bindingController = userData.mBindingController;
                 // If the system is not yet ready, we shouldn't be running third party code.
                 if (!mSystemReady) {
                     return new InputBindResult(
@@ -3706,7 +3707,6 @@
                     final boolean shouldClearFlag =
                             mImePlatformCompatUtils.shouldClearShowForcedFlag(cs.mUid);
                     final boolean showForced = mVisibilityStateComputer.mShowForced;
-                    final var userData = getUserData(userId);
                     if (userData.mImeBindingState.mFocusedWindow != windowToken
                             && showForced && shouldClearFlag) {
                         mVisibilityStateComputer.mShowForced = false;
@@ -4554,8 +4554,8 @@
     private void dumpDebug(ProtoOutputStream proto, long fieldId) {
         synchronized (ImfLock.class) {
             final int userId = mCurrentUserId;
-            final var bindingController = getInputMethodBindingController(userId);
             final var userData = getUserData(userId);
+            final var bindingController = userData.mBindingController;
             final long token = proto.start(fieldId);
             proto.write(CUR_METHOD_ID, bindingController.getSelectedMethodId());
             proto.write(CUR_SEQ, bindingController.getSequenceNumber());
@@ -4688,8 +4688,8 @@
             @UserIdInt int userId) {
         final IBinder requestToken = mVisibilityStateComputer.getWindowTokenFrom(requestImeToken,
                 userId);
-        final var bindingController = getInputMethodBindingController(userId);
         final var userData = getUserData(userId);
+        final var bindingController = userData.mBindingController;
         final WindowManagerInternal.ImeTargetInfo info =
                 mWindowManagerInternal.onToggleImeRequested(
                         show, userData.mImeBindingState.mFocusedWindow, requestToken,
@@ -5031,8 +5031,8 @@
                 final var handwritingRequest = (HandwritingRequest) msg.obj;
                 synchronized (ImfLock.class) {
                     final int userId = handwritingRequest.userId;
-                    final var bindingController = getInputMethodBindingController(userId);
                     final var userData = getUserData(userId);
+                    final var bindingController = userData.mBindingController;
                     IInputMethodInvoker curMethod = bindingController.getCurMethod();
                     if (curMethod == null || userData.mImeBindingState.mFocusedWindow == null) {
                         return true;
@@ -5089,12 +5089,12 @@
         synchronized (ImfLock.class) {
             // TODO(b/305849394): Support multiple IMEs.
             final int userId = mCurrentUserId;
-            final var bindingController = getInputMethodBindingController(userId);
+            final var userData = getUserData(userId);
+            final var bindingController = userData.mBindingController;
             mIsInteractive = interactive;
             updateSystemUiLocked(
                     interactive ? bindingController.getImeWindowVis() : 0,
                     bindingController.getBackDisposition(), userId);
-            final var userData = getUserData(userId);
             // Inform the current client of the change in active status
             if (userData.mCurClient == null || userData.mCurClient.mClient == null) {
                 return;
@@ -5772,7 +5772,7 @@
                 if (displayId != bindingController.getCurTokenDisplayId()) {
                     return false;
                 }
-                curHostInputToken = getInputMethodBindingController(userId).getCurHostInputToken();
+                curHostInputToken = bindingController.getCurHostInputToken();
                 if (curHostInputToken == null) {
                     return false;
                 }
@@ -5828,8 +5828,8 @@
         public void onSessionForAccessibilityCreated(int accessibilityConnectionId,
                 IAccessibilityInputMethodSession session, @UserIdInt int userId) {
             synchronized (ImfLock.class) {
-                final var bindingController = getInputMethodBindingController(userId);
                 final var userData = getUserData(userId);
+                final var bindingController = userData.mBindingController;
                 // TODO(b/305829876): Implement user ID verification
                 if (userData.mCurClient != null) {
                     clearClientSessionForAccessibilityLocked(userData.mCurClient,
@@ -5866,8 +5866,8 @@
         public void unbindAccessibilityFromCurrentClient(int accessibilityConnectionId,
                 @UserIdInt int userId) {
             synchronized (ImfLock.class) {
-                final var bindingController = getInputMethodBindingController(userId);
                 final var userData = getUserData(userId);
+                final var bindingController = userData.mBindingController;
                 // TODO(b/305829876): Implement user ID verification
                 if (userData.mCurClient != null) {
                     if (DEBUG) {
@@ -5935,7 +5935,8 @@
 
         synchronized (ImfLock.class) {
             final int uid = Binder.getCallingUid();
-            final var bindingController = getInputMethodBindingController(imeUserId);
+            final var userData = getUserData(imeUserId);
+            final var bindingController = userData.mBindingController;
             if (bindingController.getSelectedMethodId() == null) {
                 return null;
             }
@@ -5947,7 +5948,6 @@
             // We cannot simply distinguish a bad IME that reports an arbitrary package name from
             // an unfortunate IME whose internal state is already obsolete due to the asynchronous
             // nature of our system.  Let's compare it with our internal record.
-            final var userData = getUserData(imeUserId);
             final var curPackageName = userData.mCurEditorInfo != null
                     ? userData.mCurEditorInfo.packageName : null;
             if (!TextUtils.equals(curPackageName, packageName)) {
@@ -6106,8 +6106,8 @@
                 p.println("    pid=" + c.mPid);
             };
             mClientController.forAllClients(clientControllerDump);
-            final var bindingController = getInputMethodBindingController(mCurrentUserId);
-            p.println("  mCurrentUserId=" + mCurrentUserId);
+            final var bindingController = userData.mBindingController;
+            p.println("  mCurrentUserId=" + userData.mUserId);
             p.println("  mCurMethodId=" + bindingController.getSelectedMethodId());
             client = userData.mCurClient;
             p.println("  mCurClient=" + client + " mCurSeq="
@@ -6659,7 +6659,7 @@
                                     0 /* flags */,
                                     SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND, userId);
                         }
-                        final var bindingController = getInputMethodBindingController(userId);
+                        final var bindingController = userData.mBindingController;
                         bindingController.unbindCurrentMethod();
 
                         // Enable default IMEs, disable others
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index e122fe0..032d6b5 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -1131,9 +1131,9 @@
         }
 
         @Override
-        public void onUserUnlocked(@NonNull TargetUser user) {
-            if (user.isPreCreated()) return;
-            mService.handleOnUserUnlocked(user.getUserIdentifier());
+        public void onUserSwitching(@NonNull TargetUser from, @NonNull TargetUser to) {
+            if (to.isPreCreated()) return;
+            mService.handleOnUserSwitching(from.getUserIdentifier(), to.getUserIdentifier());
         }
     }
 
@@ -3831,8 +3831,8 @@
         mDevicePolicyEngine.handleUnlockUser(userId);
     }
 
-    void handleOnUserUnlocked(int userId) {
-        showNewUserDisclaimerIfNecessary(userId);
+    void handleOnUserSwitching(int fromUserId, int toUserId) {
+        showNewUserDisclaimerIfNecessary(toUserId);
     }
 
     void handleStopUser(int userId) {
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 753db12..b9e99dd 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -36,6 +36,8 @@
         "-Werror",
     ],
     static_libs: [
+        "a11ychecker-protos-java-proto-lite",
+        "aatf",
         "cts-input-lib",
         "frameworks-base-testutils",
         "services.accessibility",
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java
new file mode 100644
index 0000000..90d4275
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.a11ychecker;
+
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_A11Y_SERVICE_CLASS_NAME;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_A11Y_SERVICE_SOURCE_VERSION_CODE;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_ACTIVITY_NAME;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_APP_PACKAGE_NAME;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_APP_VERSION_CODE;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_WINDOW_TITLE;
+import static com.android.server.accessibility.a11ychecker.TestUtils.getMockPackageManagerWithInstalledApps;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset;
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult;
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck;
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult;
+import com.google.android.apps.common.testing.accessibility.framework.checks.ClassNameCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.ClickableSpanCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck;
+import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@RunWith(AndroidJUnit4.class)
+public class AccessibilityCheckerUtilsTest {
+
+    PackageManager mMockPackageManager;
+
+    @Before
+    public void setUp() throws PackageManager.NameNotFoundException {
+        mMockPackageManager = getMockPackageManagerWithInstalledApps();
+    }
+
+    @Test
+    public void processResults_happyPath_setsAllFields() {
+        AccessibilityNodeInfo mockNodeInfo =
+                new MockAccessibilityNodeInfoBuilder()
+                        .setViewIdResourceName("TargetNode")
+                        .build();
+        AccessibilityHierarchyCheckResult result1 =
+                new AccessibilityHierarchyCheckResult(
+                        SpeakableTextPresentCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.WARNING, null, 1,
+                        null);
+        AccessibilityHierarchyCheckResult result2 =
+                new AccessibilityHierarchyCheckResult(
+                        TouchTargetSizeCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.ERROR, null, 2, null);
+        AccessibilityHierarchyCheckResult result3 =
+                new AccessibilityHierarchyCheckResult(
+                        ClassNameCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.INFO, null, 5, null);
+        AccessibilityHierarchyCheckResult result4 =
+                new AccessibilityHierarchyCheckResult(
+                        ClickableSpanCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.NOT_RUN, null, 5,
+                        null);
+
+        Set<A11yCheckerProto.AccessibilityCheckResultReported> atoms =
+                AccessibilityCheckerUtils.processResults(
+                        mockNodeInfo,
+                        List.of(result1, result2, result3, result4),
+                        null,
+                        mMockPackageManager,
+                        new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
+                                TEST_A11Y_SERVICE_CLASS_NAME));
+
+        assertThat(atoms).containsExactly(
+                createAtom(A11yCheckerProto.AccessibilityCheckClass.SPEAKABLE_TEXT_PRESENT_CHECK,
+                        A11yCheckerProto.AccessibilityCheckResultType.WARNING, 1),
+                createAtom(A11yCheckerProto.AccessibilityCheckClass.TOUCH_TARGET_SIZE_CHECK,
+                        A11yCheckerProto.AccessibilityCheckResultType.ERROR, 2)
+        );
+    }
+
+    @Test
+    public void processResults_packageNameNotFound_returnsEmptySet()
+            throws PackageManager.NameNotFoundException {
+        when(mMockPackageManager.getPackageInfo("com.uninstalled.app", 0))
+                .thenThrow(PackageManager.NameNotFoundException.class);
+        AccessibilityNodeInfo mockNodeInfo =
+                new MockAccessibilityNodeInfoBuilder()
+                        .setPackageName("com.uninstalled.app")
+                        .setViewIdResourceName("TargetNode")
+                        .build();
+        AccessibilityHierarchyCheckResult result1 =
+                new AccessibilityHierarchyCheckResult(
+                        TouchTargetSizeCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.WARNING, null, 1,
+                        null);
+        AccessibilityHierarchyCheckResult result2 =
+                new AccessibilityHierarchyCheckResult(
+                        TouchTargetSizeCheck.class,
+                        AccessibilityCheckResult.AccessibilityCheckResultType.ERROR, null, 2, null);
+
+        Set<A11yCheckerProto.AccessibilityCheckResultReported> atoms =
+                AccessibilityCheckerUtils.processResults(
+                        mockNodeInfo,
+                        List.of(result1, result2),
+                        null,
+                        mMockPackageManager,
+                        new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
+                                TEST_A11Y_SERVICE_CLASS_NAME));
+
+        assertThat(atoms).isEmpty();
+    }
+
+    @Test
+    public void getActivityName_hasWindowStateChangedEvent_returnsActivityName() {
+        AccessibilityEvent accessibilityEvent =
+                AccessibilityEvent.obtain(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+        accessibilityEvent.setPackageName(TEST_APP_PACKAGE_NAME);
+        accessibilityEvent.setClassName(TEST_ACTIVITY_NAME);
+
+        assertThat(AccessibilityCheckerUtils.getActivityName(mMockPackageManager,
+                accessibilityEvent)).isEqualTo("MainActivity");
+    }
+
+    // Makes sure the AccessibilityHierarchyCheck class to enum mapping is up to date with the
+    // latest prod preset.
+    @Test
+    public void checkClassToEnumMap_hasAllLatestPreset() {
+        ImmutableSet<AccessibilityHierarchyCheck> checkPreset =
+                AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset(
+                        AccessibilityCheckPreset.LATEST);
+        Set<Class<? extends AccessibilityHierarchyCheck>> latestCheckClasses =
+                checkPreset.stream().map(AccessibilityHierarchyCheck::getClass).collect(
+                        Collectors.toUnmodifiableSet());
+
+        assertThat(AccessibilityCheckerUtils.CHECK_CLASS_TO_ENUM_MAP.keySet())
+                .containsExactlyElementsIn(latestCheckClasses);
+    }
+
+
+    private static A11yCheckerProto.AccessibilityCheckResultReported createAtom(
+            A11yCheckerProto.AccessibilityCheckClass checkClass,
+            A11yCheckerProto.AccessibilityCheckResultType resultType,
+            int resultId) {
+        return A11yCheckerProto.AccessibilityCheckResultReported.newBuilder()
+                .setPackageName(TEST_APP_PACKAGE_NAME)
+                .setAppVersionCode(TEST_APP_VERSION_CODE)
+                .setUiElementPath(TEST_APP_PACKAGE_NAME + ":TargetNode")
+                .setWindowTitle(TEST_WINDOW_TITLE)
+                .setActivityName("")
+                .setSourceComponentName(new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
+                        TEST_A11Y_SERVICE_CLASS_NAME).flattenToString())
+                .setSourceVersionCode(TEST_A11Y_SERVICE_SOURCE_VERSION_CODE)
+                .setResultCheckClass(checkClass)
+                .setResultType(resultType)
+                .setResultId(resultId)
+                .build();
+    }
+
+}
diff --git a/core/tests/coretests/src/android/view/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java
similarity index 80%
rename from core/tests/coretests/src/android/view/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java
rename to services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java
index 438277b..a53f42e 100644
--- a/core/tests/coretests/src/android/view/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityNodePathBuilderTest.java
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package android.view.accessibility.a11ychecker;
+package com.android.server.accessibility.a11ychecker;
 
-import static android.view.accessibility.a11ychecker.MockAccessibilityNodeInfoBuilder.PACKAGE_NAME;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_APP_PACKAGE_NAME;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -36,7 +36,7 @@
 @RunWith(AndroidJUnit4.class)
 public class AccessibilityNodePathBuilderTest {
 
-    public static final String RESOURCE_ID_PREFIX = PACKAGE_NAME + ":id/";
+    public static final String RESOURCE_ID_PREFIX = TEST_APP_PACKAGE_NAME + ":id/";
 
     @Test
     public void createNodePath_pathWithResourceNames() {
@@ -55,11 +55,11 @@
                         .build();
 
         assertThat(AccessibilityNodePathBuilder.createNodePath(child))
-                .isEqualTo(PACKAGE_NAME + ":root_node/parent_node[1]/child_node[1]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":root_node/parent_node[1]/child_node[1]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(parent))
-                .isEqualTo(PACKAGE_NAME + ":root_node/parent_node[1]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":root_node/parent_node[1]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(root))
-                .isEqualTo(PACKAGE_NAME + ":root_node");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":root_node");
     }
 
     @Test
@@ -81,11 +81,11 @@
                         .build();
 
         assertThat(AccessibilityNodePathBuilder.createNodePath(child))
-                .isEqualTo(PACKAGE_NAME + ":FrameLayout/RecyclerView[1]/TextView[1]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout/RecyclerView[1]/TextView[1]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(parent))
-                .isEqualTo(PACKAGE_NAME + ":FrameLayout/RecyclerView[1]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout/RecyclerView[1]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(root))
-                .isEqualTo(PACKAGE_NAME + ":FrameLayout");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout");
     }
 
     @Test
@@ -105,11 +105,11 @@
                         .build();
 
         assertThat(AccessibilityNodePathBuilder.createNodePath(child1))
-                .isEqualTo(PACKAGE_NAME + ":FrameLayout/child1[1]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout/child1[1]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(child2))
-                .isEqualTo(PACKAGE_NAME + ":FrameLayout/TextView[2]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout/TextView[2]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(parent))
-                .isEqualTo(PACKAGE_NAME + ":FrameLayout");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":FrameLayout");
     }
 
     @Test
@@ -133,13 +133,13 @@
                         .build();
 
         assertThat(AccessibilityNodePathBuilder.createNodePath(child1))
-                .isEqualTo(PACKAGE_NAME + ":parentId/childId[1]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":parentId/childId[1]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(child2))
-                .isEqualTo(PACKAGE_NAME + ":parentId/child/Id/With/Slash[2]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":parentId/child/Id/With/Slash[2]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(child3))
-                .isEqualTo(PACKAGE_NAME + ":parentId/childIdWithoutPrefix[3]");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":parentId/childIdWithoutPrefix[3]");
         assertThat(AccessibilityNodePathBuilder.createNodePath(parent))
-                .isEqualTo(PACKAGE_NAME + ":parentId");
+                .isEqualTo(TEST_APP_PACKAGE_NAME + ":parentId");
     }
 
 }
diff --git a/core/tests/coretests/src/android/view/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java
similarity index 72%
rename from core/tests/coretests/src/android/view/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java
rename to services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java
index e363f0c..7cd3535 100644
--- a/core/tests/coretests/src/android/view/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/MockAccessibilityNodeInfoBuilder.java
@@ -14,21 +14,33 @@
  * limitations under the License.
  */
 
-package android.view.accessibility.a11ychecker;
+package com.android.server.accessibility.a11ychecker;
+
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_APP_PACKAGE_NAME;
+import static com.android.server.accessibility.a11ychecker.TestUtils.TEST_WINDOW_TITLE;
 
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityWindowInfo;
 
 import java.util.List;
 
 final class MockAccessibilityNodeInfoBuilder {
-    static final String PACKAGE_NAME = "com.example.app";
     private final AccessibilityNodeInfo mMockNodeInfo = mock(AccessibilityNodeInfo.class);
 
     MockAccessibilityNodeInfoBuilder() {
-        when(mMockNodeInfo.getPackageName()).thenReturn(PACKAGE_NAME);
+        setPackageName(TEST_APP_PACKAGE_NAME);
+
+        AccessibilityWindowInfo windowInfo = new AccessibilityWindowInfo();
+        windowInfo.setTitle(TEST_WINDOW_TITLE);
+        when(mMockNodeInfo.getWindow()).thenReturn(windowInfo);
+    }
+
+    MockAccessibilityNodeInfoBuilder setPackageName(String packageName) {
+        when(mMockNodeInfo.getPackageName()).thenReturn(packageName);
+        return this;
     }
 
     MockAccessibilityNodeInfoBuilder setClassName(String className) {
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/OWNERS b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/OWNERS
new file mode 100644
index 0000000..7bdc029
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/OWNERS
@@ -0,0 +1 @@
+include /services/accessibility/java/com/android/server/accessibility/a11ychecker/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java
new file mode 100644
index 0000000..a04bbee
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/TestUtils.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.a11ychecker;
+
+import static org.mockito.Mockito.when;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+
+import org.mockito.Mockito;
+
+public class TestUtils {
+    static final String TEST_APP_PACKAGE_NAME = "com.example.app";
+    static final int TEST_APP_VERSION_CODE = 12321;
+    static final String TEST_ACTIVITY_NAME = "com.example.app.MainActivity";
+    static final String TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME = "com.assistive.app";
+    static final String TEST_A11Y_SERVICE_CLASS_NAME = "MyA11yService";
+    static final int TEST_A11Y_SERVICE_SOURCE_VERSION_CODE = 333555;
+    static final String TEST_WINDOW_TITLE = "Example window";
+
+    static PackageManager getMockPackageManagerWithInstalledApps()
+            throws PackageManager.NameNotFoundException {
+        PackageManager mockPackageManager = Mockito.mock(PackageManager.class);
+        ActivityInfo testActivityInfo = getTestActivityInfo();
+        ComponentName testActivityComponentName = new ComponentName(TEST_APP_PACKAGE_NAME,
+                TEST_ACTIVITY_NAME);
+
+        when(mockPackageManager.getActivityInfo(testActivityComponentName, 0))
+                .thenReturn(testActivityInfo);
+        when(mockPackageManager.getPackageInfo(TEST_APP_PACKAGE_NAME, 0))
+                .thenReturn(createPackageInfo(TEST_APP_PACKAGE_NAME, TEST_APP_VERSION_CODE,
+                        testActivityInfo));
+        when(mockPackageManager.getPackageInfo(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME, 0))
+                .thenReturn(createPackageInfo(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME,
+                        TEST_A11Y_SERVICE_SOURCE_VERSION_CODE, null));
+        return mockPackageManager;
+    }
+
+    static ActivityInfo getTestActivityInfo() {
+        ActivityInfo activityInfo = new ActivityInfo();
+        activityInfo.packageName = TEST_APP_PACKAGE_NAME;
+        activityInfo.name = TEST_ACTIVITY_NAME;
+        return activityInfo;
+    }
+
+    static PackageInfo createPackageInfo(String packageName, int versionCode,
+            @Nullable ActivityInfo activityInfo) {
+        PackageInfo packageInfo = new PackageInfo();
+        packageInfo.packageName = packageName;
+        packageInfo.setLongVersionCode(versionCode);
+        if (activityInfo != null) {
+            packageInfo.activities = new ActivityInfo[]{activityInfo};
+        }
+        return packageInfo;
+
+    }
+}