diff options
6 files changed, 206 insertions, 15 deletions
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index a7f8cb6f695e..c88da1895e8d 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -403,6 +403,8 @@ <string name="keyguard_face_failed">Can\u2019t recognize face</string> <!-- Message shown to suggest using fingerprint sensor to authenticate after another biometric failed. [CHAR LIMIT=25] --> <string name="keyguard_suggest_fingerprint">Use fingerprint instead</string> + <!-- Message shown to inform the user that face unlock is not available. [CHAR LIMIT=25] --> + <string name="keyguard_face_unlock_unavailable">Face unlock unavailable.</string> <!-- Content description of the bluetooth icon when connected for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> <string name="accessibility_bluetooth_connected">Bluetooth connected.</string> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 558869c46373..1bb83f44ce6c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -798,7 +798,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mFingerprintCancelSignal = null; updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_FP_AUTHENTICATED); - mLogger.d("onFingerprintAuthenticated"); + mLogger.logFingerprintSuccess(userId, isStrongBiometric); for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { @@ -2716,7 +2716,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab || containsFlag(strongAuth, STRONG_AUTH_REQUIRED_AFTER_TIMEOUT); // TODO: always disallow when fp is already locked out? - final boolean fpLockedout = mFingerprintLockedOut || mFingerprintLockedOutPermanent; + final boolean fpLockedOut = mFingerprintLockedOut || mFingerprintLockedOutPermanent; final boolean canBypass = mKeyguardBypassController != null && mKeyguardBypassController.canBypass(); @@ -2745,7 +2745,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab final boolean faceDisabledForUser = isFaceDisabled(user); final boolean biometricEnabledForUser = mBiometricEnabledForUser.get(user); final boolean shouldListenForFaceAssistant = shouldListenForFaceAssistant(); - final boolean fpOrFaceIsLockedOut = isFaceLockedOut() || fpLockedout; // Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an // instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware. @@ -2763,7 +2762,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab && (!mSecureCameraLaunched || mOccludingAppRequestingFace) && !faceAuthenticated && !mGoingToSleep - && !fpOrFaceIsLockedOut; + // We only care about fp locked out state and not face because we still trigger + // face auth even when face is locked out to show the user a message that face + // unlock was supposed to run but didn't + && !fpLockedOut; // Aggregate relevant fields for debug logging. maybeLogListenerModelData( @@ -2778,7 +2780,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab faceAuthenticated, faceDisabledForUser, isFaceLockedOut(), - fpLockedout, + fpLockedOut, mGoingToSleep, awakeKeyguard, mKeyguardGoingAway, diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index 3308f5550bfc..6276142d605f 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -153,6 +153,13 @@ class KeyguardUpdateMonitorLogger @Inject constructor( { "fingerprintRunningState: $int1" }) } + fun logFingerprintSuccess(userId: Int, isStrongBiometric: Boolean) { + logBuffer.log(TAG, DEBUG, { + int1 = userId + bool1 = isStrongBiometric + }, {"Fingerprint auth successful: userId: $int1, isStrongBiometric: $bool1"}) + } + fun logInvalidSubId(subId: Int) { logBuffer.log(TAG, INFO, { int1 = subId }, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 5d8232e24fe8..f2b86035200b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -79,6 +79,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.settingslib.Utils; import com.android.settingslib.fuelgauge.BatteryStatus; import com.android.systemui.R; +import com.android.systemui.biometrics.AuthController; import com.android.systemui.biometrics.FaceHelpMessageDeferral; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; @@ -137,6 +138,7 @@ public class KeyguardIndicationController { private final KeyguardStateController mKeyguardStateController; protected final StatusBarStateController mStatusBarStateController; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + private final AuthController mAuthController; private ViewGroup mIndicationArea; private KeyguardIndicationTextView mTopIndicationView; private KeyguardIndicationTextView mLockScreenIndicationView; @@ -201,6 +203,7 @@ public class KeyguardIndicationController { } } }; + private boolean mFaceLockedOutThisAuthSession; /** * Creates a new KeyguardIndicationController and registers callbacks. @@ -221,6 +224,7 @@ public class KeyguardIndicationController { @Main DelayableExecutor executor, @Background DelayableExecutor bgExecutor, FalsingManager falsingManager, + AuthController authController, LockPatternUtils lockPatternUtils, ScreenLifecycle screenLifecycle, KeyguardBypassController keyguardBypassController, @@ -240,6 +244,7 @@ public class KeyguardIndicationController { mExecutor = executor; mBackgroundExecutor = bgExecutor; mLockPatternUtils = lockPatternUtils; + mAuthController = authController; mFalsingManager = falsingManager; mKeyguardBypassController = keyguardBypassController; mAccessibilityManager = accessibilityManager; @@ -755,7 +760,7 @@ public class KeyguardIndicationController { * logic. */ private void showBiometricMessage(CharSequence biometricMessage, - CharSequence biometricMessageFollowUp) { + @Nullable CharSequence biometricMessageFollowUp) { if (TextUtils.equals(biometricMessage, mBiometricMessage)) { return; } @@ -1106,6 +1111,13 @@ public class KeyguardIndicationController { } @Override + public void onLockedOutStateChanged(BiometricSourceType biometricSourceType) { + if (biometricSourceType == FACE && !mKeyguardUpdateMonitor.isFaceLockedOut()) { + mFaceLockedOutThisAuthSession = false; + } + } + + @Override public void onBiometricError(int msgId, String errString, BiometricSourceType biometricSourceType) { if (biometricSourceType == FACE) { @@ -1124,8 +1136,10 @@ public class KeyguardIndicationController { } if (msgId == FaceManager.FACE_ERROR_TIMEOUT) { handleFaceAuthTimeoutError(deferredFaceMessage); + } else if (isLockoutError(msgId)) { + handleFaceLockoutError(errString); } else { - handleGenericBiometricError(errString); + showErrorMessageNowOrLater(errString, null); } } @@ -1134,7 +1148,7 @@ public class KeyguardIndicationController { debugLog("suppressingFingerprintError msgId=" + msgId + " errString= " + errString); } else { - handleGenericBiometricError(errString); + showErrorMessageNowOrLater(errString, null); } } @@ -1145,7 +1159,7 @@ public class KeyguardIndicationController { // pass true for isStrongBiometric to isUnlockingWithBiometricAllowed() to bypass the // check of whether non-strong biometric is allowed return ((!updateMonitor.isUnlockingWithBiometricAllowed(true /* isStrongBiometric */) - && msgId != FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT) + && !isLockoutError(msgId)) || msgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED || msgId == FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED || msgId == FingerprintManager.BIOMETRIC_ERROR_POWER_PRESSED); @@ -1234,6 +1248,31 @@ public class KeyguardIndicationController { } } + private void handleFaceLockoutError(String errString) { + int followupMsgId = canUnlockWithFingerprint() ? R.string.keyguard_suggest_fingerprint + : R.string.keyguard_unlock; + String followupMessage = mContext.getString(followupMsgId); + // Lockout error can happen multiple times in a session because we trigger face auth + // even when it is locked out so that the user is aware that face unlock would have + // triggered but didn't because it is locked out. + + // On first lockout we show the error message from FaceManager, which tells the user they + // had too many unsuccessful attempts. + if (!mFaceLockedOutThisAuthSession) { + mFaceLockedOutThisAuthSession = true; + showErrorMessageNowOrLater(errString, followupMessage); + } else if (!mAuthController.isUdfpsFingerDown()) { + // On subsequent lockouts, we show a more generic locked out message. + showBiometricMessage(mContext.getString(R.string.keyguard_face_unlock_unavailable), + followupMessage); + } + } + + private static boolean isLockoutError(int msgId) { + return msgId == FaceManager.FACE_ERROR_LOCKOUT_PERMANENT + || msgId == FaceManager.FACE_ERROR_LOCKOUT; + } + private void handleFaceAuthTimeoutError(@Nullable CharSequence deferredFaceMessage) { debugLog("showDeferredFaceMessage msgId=" + deferredFaceMessage); if (canUnlockWithFingerprint()) { @@ -1274,11 +1313,11 @@ public class KeyguardIndicationController { } } - private void handleGenericBiometricError(String errString) { + private void showErrorMessageNowOrLater(String errString, @Nullable String followUpMsg) { if (mStatusBarKeyguardViewManager.isBouncerShowing()) { mStatusBarKeyguardViewManager.setKeyguardMessage(errString, mInitialTextColorState); } else if (mScreenLifecycle.getScreenState() == SCREEN_ON) { - showBiometricMessage(errString); + showBiometricMessage(errString, followUpMsg); } else { mBiometricErrorMessageToShowOnScreenOn = errString; } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index a8284d29197d..9c14ee610b25 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -1621,7 +1621,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test - public void testShouldListenForFace_whenFaceIsLockedOut_returnsFalse() + public void testShouldListenForFace_whenFaceIsLockedOut_returnsTrue() throws RemoteException { // Preconditions for face auth to run keyguardNotGoingAway(); @@ -1638,7 +1638,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { faceAuthLockedOut(); mTestableLooper.processAllMessages(); - assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse(); + // This is needed beccause we want to show face locked out error message whenever face auth + // is supposed to run. + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java index 9a13e93ebcfc..9b17cc296081 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java @@ -20,6 +20,7 @@ import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_DEFAULT; import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED; import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE; import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_TOO_DARK; +import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT; import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_TIMEOUT; import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_RECOGNIZED; @@ -54,6 +55,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.app.Instrumentation; @@ -88,6 +90,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.settingslib.fuelgauge.BatteryStatus; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.biometrics.AuthController; import com.android.systemui.biometrics.FaceHelpMessageDeferral; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dock.DockManager; @@ -173,6 +176,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { private FaceHelpMessageDeferral mFaceHelpMessageDeferral; @Mock private ScreenLifecycle mScreenLifecycle; + @Mock + private AuthController mAuthController; @Captor private ArgumentCaptor<DockManager.AlignmentStateListener> mAlignmentListener; @Captor @@ -263,8 +268,9 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { mWakeLockBuilder, mKeyguardStateController, mStatusBarStateController, mKeyguardUpdateMonitor, mDockManager, mBroadcastDispatcher, mDevicePolicyManager, mIBatteryStats, - mUserManager, mExecutor, mExecutor, mFalsingManager, mLockPatternUtils, - mScreenLifecycle, mKeyguardBypassController, mAccessibilityManager, + mUserManager, mExecutor, mExecutor, mFalsingManager, + mAuthController, mLockPatternUtils, mScreenLifecycle, + mKeyguardBypassController, mAccessibilityManager, mFaceHelpMessageDeferral); mController.init(); mController.setIndicationArea(mIndicationArea); @@ -1371,6 +1377,110 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { } + @Test + public void onBiometricError_faceLockedOutFirstTime_showsThePassedInMessage() { + createController(); + onFaceLockoutError("first lockout"); + + verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE, "first lockout"); + } + + @Test + public void onBiometricError_faceLockedOutFirstTimeAndFpAllowed_showsTheFpFollowupMessage() { + createController(); + fingerprintUnlockIsPossible(); + onFaceLockoutError("first lockout"); + + verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP, + mContext.getString(R.string.keyguard_suggest_fingerprint)); + } + + @Test + public void onBiometricError_faceLockedOutFirstTimeAndFpNotAllowed_showsDefaultFollowup() { + createController(); + fingerprintUnlockIsNotPossible(); + onFaceLockoutError("first lockout"); + + verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP, + mContext.getString(R.string.keyguard_unlock)); + } + + @Test + public void onBiometricError_faceLockedOutSecondTimeInSession_showsUnavailableMessage() { + createController(); + onFaceLockoutError("first lockout"); + clearInvocations(mRotateTextViewController); + + onFaceLockoutError("second lockout"); + + verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE, + mContext.getString(R.string.keyguard_face_unlock_unavailable)); + } + + @Test + public void onBiometricError_faceLockedOutSecondTimeButUdfpsActive_showsNoMessage() { + createController(); + onFaceLockoutError("first lockout"); + clearInvocations(mRotateTextViewController); + + when(mAuthController.isUdfpsFingerDown()).thenReturn(true); + onFaceLockoutError("second lockout"); + + verifyNoMoreInteractions(mRotateTextViewController); + } + + @Test + public void onBiometricError_faceLockedOutAgainAndFpAllowed_showsTheFpFollowupMessage() { + createController(); + fingerprintUnlockIsPossible(); + onFaceLockoutError("first lockout"); + clearInvocations(mRotateTextViewController); + + onFaceLockoutError("second lockout"); + + verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP, + mContext.getString(R.string.keyguard_suggest_fingerprint)); + } + + @Test + public void onBiometricError_faceLockedOutAgainAndFpNotAllowed_showsDefaultFollowup() { + createController(); + fingerprintUnlockIsNotPossible(); + onFaceLockoutError("first lockout"); + clearInvocations(mRotateTextViewController); + + onFaceLockoutError("second lockout"); + + verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP, + mContext.getString(R.string.keyguard_unlock)); + } + + @Test + public void onBiometricError_whenFaceLockoutReset_onLockOutError_showsPassedInMessage() { + createController(); + onFaceLockoutError("first lockout"); + clearInvocations(mRotateTextViewController); + when(mKeyguardUpdateMonitor.isFaceLockedOut()).thenReturn(false); + mKeyguardUpdateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FACE); + + onFaceLockoutError("second lockout"); + + verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE, "second lockout"); + } + + @Test + public void onBiometricError_whenFaceIsLocked_onMultipleLockOutErrors_showUnavailableMessage() { + createController(); + onFaceLockoutError("first lockout"); + clearInvocations(mRotateTextViewController); + when(mKeyguardUpdateMonitor.isFaceLockedOut()).thenReturn(true); + mKeyguardUpdateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FACE); + + onFaceLockoutError("second lockout"); + + verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE, + mContext.getString(R.string.keyguard_face_unlock_unavailable)); + } private void sendUpdateDisclosureBroadcast() { mBroadcastReceiver.onReceive(mContext, new Intent()); @@ -1419,4 +1529,33 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { anyObject(), anyBoolean()); } } + + private void verifyIndicationShown(int indicationType, String message) { + verify(mRotateTextViewController) + .updateIndication(eq(indicationType), + mKeyguardIndicationCaptor.capture(), + eq(true)); + assertThat(mKeyguardIndicationCaptor.getValue().getMessage().toString()) + .isEqualTo(message); + } + + private void fingerprintUnlockIsNotPossible() { + setupFingerprintUnlockPossible(false); + } + + private void fingerprintUnlockIsPossible() { + setupFingerprintUnlockPossible(true); + } + + private void setupFingerprintUnlockPossible(boolean possible) { + when(mKeyguardUpdateMonitor + .getCachedIsUnlockWithFingerprintPossible(KeyguardUpdateMonitor.getCurrentUser())) + .thenReturn(possible); + } + + private void onFaceLockoutError(String errMsg) { + mKeyguardUpdateMonitorCallback.onBiometricError(FACE_ERROR_LOCKOUT_PERMANENT, + errMsg, + BiometricSourceType.FACE); + } } |