Log biometric lockout events
Test: manually checked events using go/aster-event-viewing
Test: atest KeyguardBiometricLockoutLoggerTest
Fixes: 205762820
Bug: 213483562
Bug: 213484709
Change-Id: Id639e5bdc834ee72fdea4877b73b3e8c63170bfb
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 7b8f349..56517cc 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -294,6 +294,7 @@
<string-array name="config_systemUIServiceComponents" translatable="false">
<item>com.android.systemui.util.NotificationChannels</item>
<item>com.android.systemui.keyguard.KeyguardViewMediator</item>
+ <item>com.android.keyguard.KeyguardBiometricLockoutLogger</item>
<item>com.android.systemui.recents.Recents</item>
<item>com.android.systemui.volume.VolumeUI</item>
<item>com.android.systemui.statusbar.phone.StatusBar</item>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt
new file mode 100644
index 0000000..214b284
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardBiometricLockoutLogger.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard
+
+import android.content.Context
+import android.hardware.biometrics.BiometricSourceType
+import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.logging.UiEvent
+import com.android.internal.logging.UiEventLogger
+import com.android.internal.widget.LockPatternUtils
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE
+import com.android.keyguard.KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import java.io.FileDescriptor
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * Logs events when primary authentication requirements change. Primary authentication is considered
+ * authentication using pin/pattern/password input.
+ *
+ * See [PrimaryAuthRequiredEvent] for all the events and their descriptions.
+ */
+@SysUISingleton
+class KeyguardBiometricLockoutLogger @Inject constructor(
+ context: Context?,
+ private val uiEventLogger: UiEventLogger,
+ private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+ private val dumpManager: DumpManager
+) : CoreStartable(context) {
+ private var fingerprintLockedOut = false
+ private var faceLockedOut = false
+ private var encryptedOrLockdown = false
+ private var unattendedUpdate = false
+ private var timeout = false
+
+ override fun start() {
+ dumpManager.registerDumpable(this)
+ mKeyguardUpdateMonitorCallback.onStrongAuthStateChanged(
+ KeyguardUpdateMonitor.getCurrentUser())
+ keyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback)
+ }
+
+ private val mKeyguardUpdateMonitorCallback: KeyguardUpdateMonitorCallback =
+ object : KeyguardUpdateMonitorCallback() {
+ override fun onLockedOutStateChanged(biometricSourceType: BiometricSourceType) {
+ if (biometricSourceType == BiometricSourceType.FINGERPRINT) {
+ val lockedOut = keyguardUpdateMonitor.isFingerprintLockedOut
+ if (lockedOut && !fingerprintLockedOut) {
+ uiEventLogger.log(
+ PrimaryAuthRequiredEvent.PRIMARY_AUTH_REQUIRED_FINGERPRINT_LOCKED_OUT)
+ } else if (!lockedOut && fingerprintLockedOut) {
+ uiEventLogger.log(
+ PrimaryAuthRequiredEvent
+ .PRIMARY_AUTH_REQUIRED_FINGERPRINT_LOCKED_OUT_RESET)
+ }
+ fingerprintLockedOut = lockedOut
+ } else if (biometricSourceType == BiometricSourceType.FACE) {
+ val lockedOut = keyguardUpdateMonitor.isFaceLockedOut
+ if (lockedOut && !faceLockedOut) {
+ uiEventLogger.log(
+ PrimaryAuthRequiredEvent.PRIMARY_AUTH_REQUIRED_FACE_LOCKED_OUT)
+ } else if (!lockedOut && faceLockedOut) {
+ uiEventLogger.log(
+ PrimaryAuthRequiredEvent.PRIMARY_AUTH_REQUIRED_FACE_LOCKED_OUT_RESET)
+ }
+ faceLockedOut = lockedOut
+ }
+ }
+
+ override fun onStrongAuthStateChanged(userId: Int) {
+ if (userId != KeyguardUpdateMonitor.getCurrentUser()) {
+ return
+ }
+ val strongAuthFlags = keyguardUpdateMonitor.strongAuthTracker
+ .getStrongAuthForUser(userId)
+
+ val newEncryptedOrLockdown = keyguardUpdateMonitor.isEncryptedOrLockdown(userId)
+ if (newEncryptedOrLockdown && !encryptedOrLockdown) {
+ uiEventLogger.log(
+ PrimaryAuthRequiredEvent.PRIMARY_AUTH_REQUIRED_ENCRYPTED_OR_LOCKDOWN)
+ }
+ encryptedOrLockdown = newEncryptedOrLockdown
+
+ val newUnattendedUpdate = isUnattendedUpdate(strongAuthFlags)
+ if (newUnattendedUpdate && !unattendedUpdate) {
+ uiEventLogger.log(PrimaryAuthRequiredEvent.PRIMARY_AUTH_REQUIRED_UNATTENDED_UPDATE)
+ }
+ unattendedUpdate = newUnattendedUpdate
+
+ val newTimeout = isStrongAuthTimeout(strongAuthFlags)
+ if (newTimeout && !timeout) {
+ uiEventLogger.log(PrimaryAuthRequiredEvent.PRIMARY_AUTH_REQUIRED_TIMEOUT)
+ }
+ timeout = newTimeout
+ }
+ }
+
+ private fun isUnattendedUpdate(
+ @LockPatternUtils.StrongAuthTracker.StrongAuthFlags flags: Int
+ ) = containsFlag(flags, STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE)
+
+ private fun isStrongAuthTimeout(
+ @LockPatternUtils.StrongAuthTracker.StrongAuthFlags flags: Int
+ ) = containsFlag(flags, STRONG_AUTH_REQUIRED_AFTER_TIMEOUT) ||
+ containsFlag(flags, STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT)
+
+ override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) {
+ pw.println(" mFingerprintLockedOut=$fingerprintLockedOut")
+ pw.println(" mFaceLockedOut=$faceLockedOut")
+ pw.println(" mIsEncryptedOrLockdown=$encryptedOrLockdown")
+ pw.println(" mIsUnattendedUpdate=$unattendedUpdate")
+ pw.println(" mIsTimeout=$timeout")
+ }
+
+ /**
+ * Events pertaining to whether primary authentication (pin/pattern/password input) is required
+ * for device entry.
+ */
+ @VisibleForTesting
+ enum class PrimaryAuthRequiredEvent(private val mId: Int) : UiEventLogger.UiEventEnum {
+ @UiEvent(doc = "Fingerprint cannot be used to authenticate for device entry. This" +
+ "can persist until the next primary auth or may timeout.")
+ PRIMARY_AUTH_REQUIRED_FINGERPRINT_LOCKED_OUT(924),
+
+ @UiEvent(doc = "Fingerprint can be used to authenticate for device entry.")
+ PRIMARY_AUTH_REQUIRED_FINGERPRINT_LOCKED_OUT_RESET(925),
+
+ @UiEvent(doc = "Face cannot be used to authenticate for device entry.")
+ PRIMARY_AUTH_REQUIRED_FACE_LOCKED_OUT(926),
+
+ @UiEvent(doc = "Face can be used to authenticate for device entry.")
+ PRIMARY_AUTH_REQUIRED_FACE_LOCKED_OUT_RESET(927),
+
+ @UiEvent(doc = "Device is encrypted (ie: after reboot) or device is locked down by DPM " +
+ "or a manual user lockdown.")
+ PRIMARY_AUTH_REQUIRED_ENCRYPTED_OR_LOCKDOWN(928),
+
+ @UiEvent(doc = "Primary authentication is required because it hasn't been used for a " +
+ "time required by a device admin or because primary auth hasn't been used for a " +
+ "time after a non-strong biometric (weak or convenience) is used to unlock the " +
+ "device.")
+ PRIMARY_AUTH_REQUIRED_TIMEOUT(929),
+
+ @UiEvent(doc = "Strong authentication is required to prepare for unattended upgrade.")
+ PRIMARY_AUTH_REQUIRED_UNATTENDED_UPDATE(931);
+
+ override fun getId(): Int {
+ return mId
+ }
+ }
+
+ companion object {
+ private fun containsFlag(strongAuthFlags: Int, flagCheck: Int): Boolean {
+ return strongAuthFlags and flagCheck != 0
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 98721fd..042d945 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -183,7 +183,6 @@
private static final int MSG_USER_STOPPED = 340;
private static final int MSG_USER_REMOVED = 341;
private static final int MSG_KEYGUARD_GOING_AWAY = 342;
- private static final int MSG_LOCK_SCREEN_MODE = 343;
private static final int MSG_TIME_FORMAT_UPDATE = 344;
private static final int MSG_REQUIRE_NFC_UNLOCK = 345;
@@ -221,7 +220,6 @@
private static final int BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED = -1;
public static final int BIOMETRIC_HELP_FACE_NOT_RECOGNIZED = -2;
- private static final int DEFAULT_CHARGING_VOLTAGE_MICRO_VOLT = 5000000;
/**
* If no cancel signal has been received after this amount of time, set the biometric running
* state to stopped to allow Keyguard to retry authentication.
@@ -231,7 +229,6 @@
private static final ComponentName FALLBACK_HOME_COMPONENT = new ComponentName(
"com.android.settings", "com.android.settings.FallbackHome");
-
/**
* If true, the system is in the half-boot-to-decryption-screen state.
* Prudently disable lockscreen.
@@ -1250,7 +1247,11 @@
STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
}
- private boolean isEncryptedOrLockdown(int userId) {
+ /**
+ * Returns true if primary authentication is required for the given user due to lockdown
+ * or encryption after reboot.
+ */
+ public boolean isEncryptedOrLockdown(int userId) {
final int strongAuth = mStrongAuthTracker.getStrongAuthForUser(userId);
final boolean isLockDown =
containsFlag(strongAuth, STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW)
@@ -2514,6 +2515,10 @@
return mFingerprintLockedOut || mFingerprintLockedOutPermanent;
}
+ public boolean isFaceLockedOut() {
+ return mFaceLockedOutPermanent;
+ }
+
/**
* If biometrics hardware is available, not disabled, and user has enrolled templates.
* This does NOT check if the device is encrypted or in lockdown.
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java
index 9dddbb1..c0da57f 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java
@@ -16,6 +16,7 @@
package com.android.systemui.dagger;
+import com.android.keyguard.KeyguardBiometricLockoutLogger;
import com.android.systemui.CoreStartable;
import com.android.systemui.LatencyTester;
import com.android.systemui.ScreenDecorations;
@@ -90,6 +91,13 @@
@ClassKey(KeyguardViewMediator.class)
public abstract CoreStartable bindKeyguardViewMediator(KeyguardViewMediator sysui);
+ /** Inject into KeyguardBiometricLockoutLogger. */
+ @Binds
+ @IntoMap
+ @ClassKey(KeyguardBiometricLockoutLogger.class)
+ public abstract CoreStartable bindKeyguardBiometricLockoutLogger(
+ KeyguardBiometricLockoutLogger sysui);
+
/** Inject into LatencyTests. */
@Binds
@IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index bfa4a24..dee1b33 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -11,7 +11,7 @@
* 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
+ * limitations under the License.
*/
package com.android.systemui.statusbar.phone;
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt
new file mode 100644
index 0000000..6bc6505
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardBiometricLockoutLoggerTest.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard
+
+import android.hardware.biometrics.BiometricSourceType
+import org.mockito.Mockito.verify
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class KeyguardBiometricLockoutLoggerTest : SysuiTestCase() {
+ @Mock
+ lateinit var uiEventLogger: UiEventLogger
+ @Mock
+ lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+ @Mock
+ lateinit var dumpManager: DumpManager
+ @Mock
+ lateinit var strongAuthTracker: KeyguardUpdateMonitor.StrongAuthTracker
+
+ @Captor
+ lateinit var updateMonitorCallbackCaptor: ArgumentCaptor<KeyguardUpdateMonitorCallback>
+ lateinit var updateMonitorCallback: KeyguardUpdateMonitorCallback
+
+ lateinit var keyguardBiometricLockoutLogger: KeyguardBiometricLockoutLogger
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(keyguardUpdateMonitor.strongAuthTracker).thenReturn(strongAuthTracker)
+ keyguardBiometricLockoutLogger = KeyguardBiometricLockoutLogger(
+ mContext,
+ uiEventLogger,
+ keyguardUpdateMonitor,
+ dumpManager)
+ }
+
+ @Test
+ fun test_logsOnStart() {
+ // GIVEN is encrypted / lockdown before start
+ whenever(keyguardUpdateMonitor.isEncryptedOrLockdown(anyInt()))
+ .thenReturn(true)
+
+ // WHEN start
+ keyguardBiometricLockoutLogger.start()
+
+ // THEN encrypted / lockdown state is logged
+ verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent
+ .PRIMARY_AUTH_REQUIRED_ENCRYPTED_OR_LOCKDOWN)
+ }
+
+ @Test
+ fun test_logTimeoutChange() {
+ keyguardBiometricLockoutLogger.start()
+ captureUpdateMonitorCallback()
+
+ // GIVEN primary auth required b/c timeout
+ whenever(strongAuthTracker.getStrongAuthForUser(anyInt()))
+ .thenReturn(STRONG_AUTH_REQUIRED_AFTER_TIMEOUT)
+
+ // WHEN primary auth requirement changes
+ updateMonitorCallback.onStrongAuthStateChanged(0)
+
+ // THEN primary auth required state is logged
+ verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent
+ .PRIMARY_AUTH_REQUIRED_TIMEOUT)
+ }
+
+ @Test
+ fun test_logUnattendedUpdate() {
+ keyguardBiometricLockoutLogger.start()
+ captureUpdateMonitorCallback()
+
+ // GIVEN primary auth required b/c unattended update
+ whenever(strongAuthTracker.getStrongAuthForUser(anyInt()))
+ .thenReturn(STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE)
+
+ // WHEN primary auth requirement changes
+ updateMonitorCallback.onStrongAuthStateChanged(0)
+
+ // THEN primary auth required state is logged
+ verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent
+ .PRIMARY_AUTH_REQUIRED_UNATTENDED_UPDATE)
+ }
+
+ @Test
+ fun test_logMultipleChanges() {
+ keyguardBiometricLockoutLogger.start()
+ captureUpdateMonitorCallback()
+
+ // GIVEN primary auth required b/c timeout
+ whenever(strongAuthTracker.getStrongAuthForUser(anyInt()))
+ .thenReturn(STRONG_AUTH_REQUIRED_AFTER_TIMEOUT
+ or STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE)
+
+ // WHEN primary auth requirement changes
+ updateMonitorCallback.onStrongAuthStateChanged(0)
+
+ // THEN primary auth required state is logged with all the reasons
+ verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent
+ .PRIMARY_AUTH_REQUIRED_TIMEOUT)
+ verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent
+ .PRIMARY_AUTH_REQUIRED_UNATTENDED_UPDATE)
+
+ // WHEN onStrongAuthStateChanged is called again
+ updateMonitorCallback.onStrongAuthStateChanged(0)
+
+ // THEN no more events are sent since there haven't been any changes
+ verifyNoMoreInteractions(uiEventLogger)
+ }
+
+ @Test
+ fun test_logFaceLockout() {
+ keyguardBiometricLockoutLogger.start()
+ captureUpdateMonitorCallback()
+
+ // GIVEN primary auth required b/c face lock
+ whenever(keyguardUpdateMonitor.isFaceLockedOut).thenReturn(true)
+
+ // WHEN lockout state changes
+ updateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FACE)
+
+ // THEN primary auth required state is logged
+ verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent
+ .PRIMARY_AUTH_REQUIRED_FACE_LOCKED_OUT)
+
+ // WHEN face lockout is reset
+ whenever(keyguardUpdateMonitor.isFaceLockedOut).thenReturn(false)
+ updateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FACE)
+
+ // THEN primary auth required state is logged
+ verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent
+ .PRIMARY_AUTH_REQUIRED_FACE_LOCKED_OUT_RESET)
+ }
+
+ @Test
+ fun test_logFingerprintLockout() {
+ keyguardBiometricLockoutLogger.start()
+ captureUpdateMonitorCallback()
+
+ // GIVEN primary auth required b/c fingerprint lock
+ whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(true)
+
+ // WHEN lockout state changes
+ updateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FINGERPRINT)
+
+ // THEN primary auth required state is logged
+ verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent
+ .PRIMARY_AUTH_REQUIRED_FINGERPRINT_LOCKED_OUT)
+
+ // WHEN fingerprint lockout is reset
+ whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(false)
+ updateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FINGERPRINT)
+
+ // THEN primary auth required state is logged
+ verify(uiEventLogger).log(KeyguardBiometricLockoutLogger.PrimaryAuthRequiredEvent
+ .PRIMARY_AUTH_REQUIRED_FINGERPRINT_LOCKED_OUT_RESET)
+ }
+
+ fun captureUpdateMonitorCallback() {
+ verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallbackCaptor.capture())
+ updateMonitorCallback = updateMonitorCallbackCaptor.value
+ }
+}
\ No newline at end of file