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;
+
+ }
+}