diff options
11 files changed, 876 insertions, 93 deletions
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 350c4ed084b9..33a822440866 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -154,6 +154,15 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.dump.DumpsysTableLogger; +import com.android.systemui.keyguard.domain.interactor.FaceAuthenticationListener; +import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; +import com.android.systemui.keyguard.shared.model.AcquiredAuthenticationStatus; +import com.android.systemui.keyguard.shared.model.AuthenticationStatus; +import com.android.systemui.keyguard.shared.model.DetectionStatus; +import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus; +import com.android.systemui.keyguard.shared.model.FailedAuthenticationStatus; +import com.android.systemui.keyguard.shared.model.HelpAuthenticationStatus; +import com.android.systemui.keyguard.shared.model.SuccessAuthenticationStatus; import com.android.systemui.keyguard.shared.model.SysUiFaceAuthenticateOptions; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.WeatherData; @@ -372,6 +381,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final FingerprintManager mFpm; @Nullable private final FaceManager mFaceManager; + @Nullable + private KeyguardFaceAuthInteractor mFaceAuthInteractor; private final LockPatternUtils mLockPatternUtils; @VisibleForTesting @DevicePostureInt @@ -1165,8 +1176,21 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab Trace.endSection(); } + /** + * @deprecated This is being migrated to use modern architecture, this method is visible purely + * for bridging the gap while the migration is active. + */ private void handleFaceAuthFailed() { Assert.isMainThread(); + String reason = + mKeyguardBypassController.canBypass() ? "bypass" + : mAlternateBouncerShowing ? "alternateBouncer" + : mPrimaryBouncerFullyShown ? "bouncer" + : "udfpsFpDown"; + requestActiveUnlock( + ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL, + "faceFailure-" + reason); + mLogger.d("onFaceAuthFailed"); mFaceCancelSignal = null; setFaceRunningState(BIOMETRIC_STATE_STOPPED); @@ -1180,6 +1204,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mContext.getString(R.string.kg_face_not_recognized)); } + /** + * @deprecated This is being migrated to use modern architecture, this method is visible purely + * for bridging the gap while the migration is active. + */ private void handleFaceAcquired(int acquireInfo) { Assert.isMainThread(); mLogger.logFaceAcquired(acquireInfo); @@ -1189,8 +1217,19 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab cb.onBiometricAcquired(FACE, acquireInfo); } } + + if (mActiveUnlockConfig.shouldRequestActiveUnlockOnFaceAcquireInfo( + acquireInfo)) { + requestActiveUnlock( + ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL, + "faceAcquireInfo-" + acquireInfo); + } } + /** + * @deprecated This is being migrated to use modern architecture, this method is visible purely + * for bridging the gap while the migration is active. + */ private void handleFaceAuthenticated(int authUserId, boolean isStrongBiometric) { Trace.beginSection("KeyGuardUpdateMonitor#handlerFaceAuthenticated"); try { @@ -1203,7 +1242,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mLogger.logFaceAuthForWrongUser(authUserId); return; } - if (isFaceDisabled(userId)) { + if (!isFaceAuthInteractorEnabled() && isFaceDisabled(userId)) { mLogger.logFaceAuthDisabledForUser(userId); return; } @@ -1215,6 +1254,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab Trace.endSection(); } + /** + * @deprecated This is being migrated to use modern architecture, this method is visible purely + * for bridging the gap while the migration is active. + */ private void handleFaceHelp(int msgId, String helpString) { Assert.isMainThread(); mLogger.logFaceAuthHelpMsg(msgId, helpString); @@ -1226,22 +1269,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } - private final Runnable mRetryFaceAuthentication = new Runnable() { - @Override - public void run() { - mLogger.logRetryingAfterFaceHwUnavailable(mHardwareFaceUnavailableRetryCount); - updateFaceListeningState(BIOMETRIC_ACTION_UPDATE, - FACE_AUTH_TRIGGERED_RETRY_AFTER_HW_UNAVAILABLE); - } - }; - - private void onFaceCancelNotReceived() { - mLogger.e("Face cancellation not received, transitioning to STOPPED"); - mFaceRunningState = BIOMETRIC_STATE_STOPPED; - KeyguardUpdateMonitor.this.updateFaceListeningState(BIOMETRIC_ACTION_STOP, - FACE_AUTH_STOPPED_FACE_CANCEL_NOT_RECEIVED); - } - + /** + * @deprecated This is being migrated to use modern architecture, this method is visible purely + * for bridging the gap while the migration is active. + */ private void handleFaceError(int msgId, final String originalErrMsg) { Assert.isMainThread(); String errString = originalErrMsg; @@ -1299,6 +1330,28 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab if (lockedOutStateChanged) { notifyLockedOutStateChanged(FACE); } + + if (mActiveUnlockConfig.shouldRequestActiveUnlockOnFaceError(msgId)) { + requestActiveUnlock( + ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL, + "faceError-" + msgId); + } + } + + private final Runnable mRetryFaceAuthentication = new Runnable() { + @Override + public void run() { + mLogger.logRetryingAfterFaceHwUnavailable(mHardwareFaceUnavailableRetryCount); + updateFaceListeningState(BIOMETRIC_ACTION_UPDATE, + FACE_AUTH_TRIGGERED_RETRY_AFTER_HW_UNAVAILABLE); + } + }; + + private void onFaceCancelNotReceived() { + mLogger.e("Face cancellation not received, transitioning to STOPPED"); + mFaceRunningState = BIOMETRIC_STATE_STOPPED; + KeyguardUpdateMonitor.this.updateFaceListeningState(BIOMETRIC_ACTION_STOP, + FACE_AUTH_STOPPED_FACE_CANCEL_NOT_RECEIVED); } private void handleFaceLockoutReset(@LockoutMode int mode) { @@ -1343,10 +1396,61 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return mFingerprintRunningState == BIOMETRIC_STATE_RUNNING; } + /** + * @deprecated This is being migrated to use modern architecture. + */ + @Deprecated public boolean isFaceDetectionRunning() { + if (isFaceAuthInteractorEnabled()) { + return getFaceAuthInteractor().isRunning(); + } return mFaceRunningState == BIOMETRIC_STATE_RUNNING; } + private boolean isFaceAuthInteractorEnabled() { + return mFaceAuthInteractor != null && mFaceAuthInteractor.isEnabled(); + } + + private @Nullable KeyguardFaceAuthInteractor getFaceAuthInteractor() { + return mFaceAuthInteractor; + } + + /** + * Set the face auth interactor that should be used for initiating face authentication. + */ + public void setFaceAuthInteractor(@Nullable KeyguardFaceAuthInteractor faceAuthInteractor) { + mFaceAuthInteractor = faceAuthInteractor; + mFaceAuthInteractor.registerListener(mFaceAuthenticationListener); + } + + private FaceAuthenticationListener mFaceAuthenticationListener = + new FaceAuthenticationListener() { + @Override + public void onAuthenticationStatusChanged(@NonNull AuthenticationStatus status) { + if (status instanceof AcquiredAuthenticationStatus) { + handleFaceAcquired( + ((AcquiredAuthenticationStatus) status).getAcquiredInfo()); + } else if (status instanceof ErrorAuthenticationStatus) { + ErrorAuthenticationStatus error = (ErrorAuthenticationStatus) status; + handleFaceError(error.getMsgId(), error.getMsg()); + } else if (status instanceof FailedAuthenticationStatus) { + handleFaceAuthFailed(); + } else if (status instanceof HelpAuthenticationStatus) { + HelpAuthenticationStatus helpMsg = (HelpAuthenticationStatus) status; + handleFaceHelp(helpMsg.getMsgId(), helpMsg.getMsg()); + } else if (status instanceof SuccessAuthenticationStatus) { + FaceManager.AuthenticationResult result = + ((SuccessAuthenticationStatus) status).getSuccessResult(); + handleFaceAuthenticated(result.getUserId(), result.isStrongBiometric()); + } + } + + @Override + public void onDetectionStatusChanged(@NonNull DetectionStatus status) { + handleFaceAuthenticated(status.getUserId(), status.isStrongBiometric()); + } + }; + private boolean isTrustDisabled() { // Don't allow trust agent if device is secured with a SIM PIN. This is here // mainly because there's no other way to prompt the user to enter their SIM PIN @@ -1360,6 +1464,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab || isSimPinSecure(); } + /** + * @deprecated This method is not needed anymore with the new face auth system. + */ + @Deprecated private boolean isFaceDisabled(int userId) { // TODO(b/140035044) return whitelistIpcs(() -> @@ -1371,7 +1479,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab /** * @return whether the current user has been authenticated with face. This may be true * on the lockscreen if the user doesn't have bypass enabled. + * + * @deprecated This is being migrated to use modern architecture. */ + @Deprecated public boolean getIsFaceAuthenticated() { boolean faceAuthenticated = false; BiometricAuthenticated bioFaceAuthenticated = mUserFaceAuthenticated.get(getCurrentUser()); @@ -1619,6 +1730,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab void setAssistantVisible(boolean assistantVisible) { mAssistantVisible = assistantVisible; mLogger.logAssistantVisible(mAssistantVisible); + if (isFaceAuthInteractorEnabled()) { + mFaceAuthInteractor.onAssistantTriggeredOnLockScreen(); + } updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_ASSISTANT_VISIBILITY_CHANGED); if (mAssistantVisible) { @@ -1832,54 +1946,27 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @Override public void onAuthenticationFailed() { - String reason = - mKeyguardBypassController.canBypass() ? "bypass" - : mAlternateBouncerShowing ? "alternateBouncer" - : mPrimaryBouncerFullyShown ? "bouncer" - : "udfpsFpDown"; - requestActiveUnlock( - ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL, - "faceFailure-" + reason); - handleFaceAuthFailed(); } @Override public void onAuthenticationSucceeded(FaceManager.AuthenticationResult result) { - Trace.beginSection("KeyguardUpdateMonitor#onAuthenticationSucceeded"); handleFaceAuthenticated(result.getUserId(), result.isStrongBiometric()); - Trace.endSection(); } @Override public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { - if (mFaceAcquiredInfoIgnoreList.contains(helpMsgId)) { - return; - } handleFaceHelp(helpMsgId, helpString.toString()); } @Override public void onAuthenticationError(int errMsgId, CharSequence errString) { handleFaceError(errMsgId, errString.toString()); - - if (mActiveUnlockConfig.shouldRequestActiveUnlockOnFaceError(errMsgId)) { - requestActiveUnlock( - ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL, - "faceError-" + errMsgId); - } } @Override public void onAuthenticationAcquired(int acquireInfo) { handleFaceAcquired(acquireInfo); - - if (mActiveUnlockConfig.shouldRequestActiveUnlockOnFaceAcquireInfo( - acquireInfo)) { - requestActiveUnlock( - ActiveUnlockConfig.ActiveUnlockRequestOrigin.BIOMETRIC_FAIL, - "faceAcquireInfo-" + acquireInfo); - } } }; @@ -2628,7 +2715,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * @param reason One of the reasons {@link FaceAuthApiRequestReason} on why this API is being * invoked. * @return current face auth detection state, true if it is running. + * @deprecated This is being migrated to use modern architecture. */ + @Deprecated public boolean requestFaceAuth(@FaceAuthApiRequestReason String reason) { mLogger.logFaceAuthRequested(reason); updateFaceListeningState(BIOMETRIC_ACTION_START, apiRequestReasonToUiEvent(reason)); @@ -2643,6 +2732,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } private void updateFaceListeningState(int action, @NonNull FaceAuthUiEvent faceAuthUiEvent) { + if (isFaceAuthInteractorEnabled()) return; // If this message exists, we should not authenticate again until this message is // consumed by the handler if (mHandler.hasMessages(MSG_BIOMETRIC_AUTHENTICATION_CONTINUE)) { @@ -3154,6 +3244,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } public boolean isFaceLockedOut() { + if (isFaceAuthInteractorEnabled()) { + return getFaceAuthInteractor().isLockedOut(); + } return mFaceLockedOutPermanent; } @@ -3202,13 +3295,23 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return mIsUnlockWithFingerprintPossible.getOrDefault(userId, false); } + /** + * @deprecated This is being migrated to use modern architecture. + */ + @Deprecated private boolean isUnlockWithFacePossible(int userId) { + if (isFaceAuthInteractorEnabled()) { + return getFaceAuthInteractor().canFaceAuthRun(); + } return isFaceAuthEnabledForUser(userId) && !isFaceDisabled(userId); } /** * If face hardware is available, user has enrolled and enabled auth via setting. + * + * @deprecated This is being migrated to use modern architecture. */ + @Deprecated public boolean isFaceAuthEnabledForUser(int userId) { // TODO (b/242022358), make this rely on onEnrollmentChanged event and update it only once. updateFaceEnrolled(userId); @@ -3232,6 +3335,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } private void stopListeningForFace(@NonNull FaceAuthUiEvent faceAuthUiEvent) { + if (isFaceAuthInteractorEnabled()) return; mLogger.v("stopListeningForFace()"); mLogger.logStoppedListeningForFace(mFaceRunningState, faceAuthUiEvent.getReason()); mUiEventLogger.log(faceAuthUiEvent, getKeyguardSessionId()); @@ -4098,6 +4202,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mStatusBarStateController.removeCallback(mStatusBarStateControllerListener); mTelephonyListenerManager.removeActiveDataSubscriptionIdListener(mPhoneStateListener); mSubscriptionManager.removeOnSubscriptionsChangedListener(mSubscriptionListener); + if (isFaceAuthInteractorEnabled()) { + mFaceAuthInteractor.unregisterListener(mFaceAuthenticationListener); + } if (mDeviceProvisionedObserver != null) { mContext.getContentResolver().unregisterContentObserver(mDeviceProvisionedObserver); @@ -4127,6 +4234,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab pw.println(" getUserHasTrust()=" + getUserHasTrust(getCurrentUser())); pw.println(" getUserUnlockedWithBiometric()=" + getUserUnlockedWithBiometric(getCurrentUser())); + pw.println(" isFaceAuthInteractorEnabled: " + isFaceAuthInteractorEnabled()); pw.println(" SIM States:"); for (SimData data : mSimDatas.values()) { pw.println(" " + data.toString()); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt index 0abce82527f9..c4fc8834df83 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt @@ -44,6 +44,8 @@ import com.android.systemui.keyguard.shared.model.SuccessAuthenticationStatus import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.log.FaceAuthenticationLogger import com.android.systemui.log.SessionTracker +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.user.data.repository.UserRepository import java.io.PrintWriter @@ -78,7 +80,7 @@ interface DeviceEntryFaceAuthRepository { val isAuthenticated: Flow<Boolean> /** Whether face auth can run at this point. */ - val canRunFaceAuth: Flow<Boolean> + val canRunFaceAuth: StateFlow<Boolean> /** Provide the current status of face authentication. */ val authenticationStatus: Flow<AuthenticationStatus> @@ -87,10 +89,10 @@ interface DeviceEntryFaceAuthRepository { val detectionStatus: Flow<DetectionStatus> /** Current state of whether face authentication is locked out or not. */ - val isLockedOut: Flow<Boolean> + val isLockedOut: StateFlow<Boolean> /** Current state of whether face authentication is running. */ - val isAuthRunning: Flow<Boolean> + val isAuthRunning: StateFlow<Boolean> /** Whether bypass is currently enabled */ val isBypassEnabled: Flow<Boolean> @@ -129,6 +131,8 @@ constructor( private val keyguardRepository: KeyguardRepository, private val keyguardInteractor: KeyguardInteractor, private val alternateBouncerInteractor: AlternateBouncerInteractor, + @FaceDetectTableLog private val faceDetectLog: TableLogBuffer, + @FaceAuthTableLog private val faceAuthLog: TableLogBuffer, dumpManager: DumpManager, ) : DeviceEntryFaceAuthRepository, Dumpable { private var authCancellationSignal: CancellationSignal? = null @@ -224,17 +228,19 @@ constructor( // Face detection can run only when lockscreen bypass is enabled // & detection is supported & biometric unlock is not allowed. listOf( - canFaceAuthOrDetectRun(), - logAndObserve(isBypassEnabled, "isBypassEnabled"), + canFaceAuthOrDetectRun(faceDetectLog), + logAndObserve(isBypassEnabled, "isBypassEnabled", faceDetectLog), logAndObserve( biometricSettingsRepository.isNonStrongBiometricAllowed.isFalse(), - "nonStrongBiometricIsNotAllowed" + "nonStrongBiometricIsNotAllowed", + faceDetectLog ), // We don't want to run face detect if it's not possible to authenticate with FP // from the bouncer. UDFPS is the only fp sensor type that won't support this. logAndObserve( and(isUdfps(), deviceEntryFingerprintAuthRepository.isRunning).isFalse(), - "udfpsAuthIsNotPossibleAnymore" + "udfpsAuthIsNotPossibleAnymore", + faceDetectLog ) ) .reduce(::and) @@ -246,6 +252,7 @@ constructor( cancelDetection() } } + .logDiffsForTable(faceDetectLog, "", "canFaceDetectRun", false) .launchIn(applicationScope) } @@ -254,26 +261,34 @@ constructor( it == BiometricType.UNDER_DISPLAY_FINGERPRINT } - private fun canFaceAuthOrDetectRun(): Flow<Boolean> { + private fun canFaceAuthOrDetectRun(tableLogBuffer: TableLogBuffer): Flow<Boolean> { return listOf( - logAndObserve(biometricSettingsRepository.isFaceEnrolled, "isFaceEnrolled"), + logAndObserve( + biometricSettingsRepository.isFaceEnrolled, + "isFaceEnrolled", + tableLogBuffer + ), logAndObserve( biometricSettingsRepository.isFaceAuthenticationEnabled, - "isFaceAuthenticationEnabled" + "isFaceAuthenticationEnabled", + tableLogBuffer ), logAndObserve( userRepository.userSwitchingInProgress.isFalse(), - "userSwitchingNotInProgress" + "userSwitchingNotInProgress", + tableLogBuffer ), logAndObserve( keyguardRepository.isKeyguardGoingAway.isFalse(), - "keyguardNotGoingAway" + "keyguardNotGoingAway", + tableLogBuffer ), logAndObserve( keyguardRepository.wakefulness .map { WakefulnessModel.isSleepingOrStartingToSleep(it) } .isFalse(), - "deviceNotSleepingOrNotStartingToSleep" + "deviceNotSleepingOrNotStartingToSleep", + tableLogBuffer ), logAndObserve( combine( @@ -282,15 +297,18 @@ constructor( ) { a, b -> !a || b }, - "secureCameraNotActiveOrAltBouncerIsShowing" + "secureCameraNotActiveOrAltBouncerIsShowing", + tableLogBuffer ), logAndObserve( biometricSettingsRepository.isFaceAuthSupportedInCurrentPosture, - "isFaceAuthSupportedInCurrentPosture" + "isFaceAuthSupportedInCurrentPosture", + tableLogBuffer ), logAndObserve( biometricSettingsRepository.isCurrentUserInLockdown.isFalse(), - "userHasNotLockedDownDevice" + "userHasNotLockedDownDevice", + tableLogBuffer ) ) .reduce(::and) @@ -299,20 +317,27 @@ constructor( private fun observeFaceAuthGatingChecks() { // Face auth can run only if all of the gating conditions are true. listOf( - canFaceAuthOrDetectRun(), - logAndObserve(isLockedOut.isFalse(), "isNotLocked"), + canFaceAuthOrDetectRun(faceAuthLog), + logAndObserve(isLockedOut.isFalse(), "isNotInLockOutState", faceAuthLog), logAndObserve( deviceEntryFingerprintAuthRepository.isLockedOut.isFalse(), - "fpLockedOut" + "fpLockedOut", + faceAuthLog + ), + logAndObserve( + trustRepository.isCurrentUserTrusted.isFalse(), + "currentUserTrusted", + faceAuthLog ), - logAndObserve(trustRepository.isCurrentUserTrusted.isFalse(), "currentUserTrusted"), logAndObserve( biometricSettingsRepository.isNonStrongBiometricAllowed, - "nonStrongBiometricIsAllowed" + "nonStrongBiometricIsAllowed", + faceAuthLog ), logAndObserve( userRepository.selectedUserInfo.map { it.isPrimary }, - "userIsPrimaryUser" + "userIsPrimaryUser", + faceAuthLog ), ) .reduce(::and) @@ -326,6 +351,7 @@ constructor( cancel() } } + .logDiffsForTable(faceAuthLog, "", "canFaceAuthRun", false) .launchIn(applicationScope) } @@ -340,7 +366,6 @@ constructor( override fun onAuthenticationAcquired(acquireInfo: Int) { _authenticationStatus.value = AcquiredAuthenticationStatus(acquireInfo) - faceAuthLogger.authenticationAcquired(acquireInfo) } override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) { @@ -401,7 +426,7 @@ constructor( override suspend fun authenticate(uiEvent: FaceAuthUiEvent, fallbackToDetection: Boolean) { if (_isAuthRunning.value) { - faceAuthLogger.ignoredFaceAuthTrigger(uiEvent) + faceAuthLogger.ignoredFaceAuthTrigger(uiEvent, "face auth is currently running") return } @@ -438,7 +463,16 @@ constructor( ) } } else if (fallbackToDetection && canRunDetection.value) { + faceAuthLogger.ignoredFaceAuthTrigger( + uiEvent, + "face auth gating check is false, falling back to detection." + ) detect() + } else { + faceAuthLogger.ignoredFaceAuthTrigger( + uiEvent, + "face auth & detect gating check is false" + ) } } @@ -467,7 +501,7 @@ constructor( private val currentUserId: Int get() = userRepository.getSelectedUserInfo().id - fun cancelDetection() { + private fun cancelDetection() { detectCancellationSignal?.cancel() detectCancellationSignal = null } @@ -491,10 +525,20 @@ constructor( _isAuthRunning.value = false } - private fun logAndObserve(cond: Flow<Boolean>, loggingContext: String): Flow<Boolean> { - return cond.distinctUntilChanged().onEach { - faceAuthLogger.observedConditionChanged(it, loggingContext) - } + private fun logAndObserve( + cond: Flow<Boolean>, + conditionName: String, + logBuffer: TableLogBuffer + ): Flow<Boolean> { + return cond + .distinctUntilChanged() + .logDiffsForTable( + logBuffer, + columnName = conditionName, + columnPrefix = "", + initialValue = false + ) + .onEach { faceAuthLogger.observedConditionChanged(it, conditionName) } } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/FaceAuthTableLog.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/FaceAuthTableLog.kt new file mode 100644 index 000000000000..6c23032143c1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/FaceAuthTableLog.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import javax.inject.Qualifier + +/** Face auth logs in table format. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class FaceAuthTableLog diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/FaceDetectTableLog.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/FaceDetectTableLog.kt new file mode 100644 index 000000000000..342064f02ef2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/FaceDetectTableLog.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import javax.inject.Qualifier + +/** Face detect logs in table format. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class FaceDetectTableLog diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt index 3c66f2424c7b..1a72375457dd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt @@ -17,8 +17,16 @@ package com.android.systemui.keyguard.data.repository +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.TableLogBufferFactory import dagger.Binds import dagger.Module +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap @Module interface KeyguardFaceAuthModule { @@ -27,5 +35,26 @@ interface KeyguardFaceAuthModule { impl: DeviceEntryFaceAuthRepositoryImpl ): DeviceEntryFaceAuthRepository + @Binds + @IntoMap + @ClassKey(KeyguardFaceAuthInteractor::class) + fun bind(impl: KeyguardFaceAuthInteractor): CoreStartable + @Binds fun trustRepository(impl: TrustRepositoryImpl): TrustRepository + + companion object { + @Provides + @SysUISingleton + @FaceAuthTableLog + fun provideFaceAuthTableLog(factory: TableLogBufferFactory): TableLogBuffer { + return factory.create("FaceAuthTableLog", 100) + } + + @Provides + @SysUISingleton + @FaceDetectTableLog + fun provideFaceDetectTableLog(factory: TableLogBufferFactory): TableLogBuffer { + return factory.create("FaceDetectTableLog", 100) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt new file mode 100644 index 000000000000..7a21be187dcf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.keyguard.FaceAuthUiEvent +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.shared.model.AuthenticationStatus +import com.android.systemui.keyguard.shared.model.DetectionStatus +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.log.FaceAuthenticationLogger +import com.android.systemui.util.kotlin.pairwise +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch + +/** + * Encapsulates business logic related face authentication being triggered for device entry from + * keyguard + */ +@SysUISingleton +class KeyguardFaceAuthInteractor +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + @Main private val mainDispatcher: CoroutineDispatcher, + private val repository: DeviceEntryFaceAuthRepository, + private val primaryBouncerInteractor: PrimaryBouncerInteractor, + private val alternateBouncerInteractor: AlternateBouncerInteractor, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, + private val featureFlags: FeatureFlags, + private val faceAuthenticationLogger: FaceAuthenticationLogger, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, +) : CoreStartable { + + private val listeners: MutableList<FaceAuthenticationListener> = mutableListOf() + + override fun start() { + if (!isEnabled()) { + return + } + // This is required because fingerprint state required for the face auth repository is + // backed by KeyguardUpdateMonitor. KeyguardUpdateMonitor constructor accesses the biometric + // state which makes lazy injection not an option. + keyguardUpdateMonitor.setFaceAuthInteractor(this) + observeFaceAuthStateUpdates() + faceAuthenticationLogger.interactorStarted() + primaryBouncerInteractor.isShowing + .whenItFlipsToTrue() + .onEach { + faceAuthenticationLogger.bouncerVisibilityChanged() + runFaceAuth( + FaceAuthUiEvent.FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN, + fallbackToDetect = true + ) + } + .launchIn(applicationScope) + + alternateBouncerInteractor.isVisible + .whenItFlipsToTrue() + .onEach { + faceAuthenticationLogger.alternateBouncerVisibilityChanged() + runFaceAuth( + FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN, + fallbackToDetect = false + ) + } + .launchIn(applicationScope) + + merge( + keyguardTransitionInteractor.aodToLockscreenTransition, + keyguardTransitionInteractor.offToLockscreenTransition, + keyguardTransitionInteractor.dozingToLockscreenTransition + ) + .filter { it.transitionState == TransitionState.STARTED } + .onEach { + faceAuthenticationLogger.lockscreenBecameVisible(it) + runFaceAuth( + FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED, + fallbackToDetect = true + ) + } + .launchIn(applicationScope) + } + + private fun runFaceAuth(uiEvent: FaceAuthUiEvent, fallbackToDetect: Boolean) { + if (featureFlags.isEnabled(Flags.FACE_AUTH_REFACTOR)) { + applicationScope.launch { + faceAuthenticationLogger.authRequested(uiEvent) + repository.authenticate(uiEvent, fallbackToDetection = fallbackToDetect) + } + } else { + faceAuthenticationLogger.ignoredFaceAuthTrigger( + uiEvent, + ignoredReason = "Skipping face auth request because feature flag is false" + ) + } + } + + fun onSwipeUpOnBouncer() { + runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, false) + } + + fun onNotificationPanelClicked() { + runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED, true) + } + + fun onQsExpansionStared() { + runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED, true) + } + + fun onDeviceLifted() { + runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED, true) + } + + fun onAssistantTriggeredOnLockScreen() { + runFaceAuth(FaceAuthUiEvent.FACE_AUTH_UPDATED_ASSISTANT_VISIBILITY_CHANGED, true) + } + + fun onUdfpsSensorTouched() { + runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_UDFPS_POINTER_DOWN, false) + } + + fun registerListener(listener: FaceAuthenticationListener) { + listeners.add(listener) + } + + fun unregisterListener(listener: FaceAuthenticationListener) { + listeners.remove(listener) + } + + fun isLockedOut(): Boolean = repository.isLockedOut.value + + fun isRunning(): Boolean = repository.isAuthRunning.value + + fun canFaceAuthRun(): Boolean = repository.canRunFaceAuth.value + + fun isEnabled(): Boolean { + return featureFlags.isEnabled(Flags.FACE_AUTH_REFACTOR) + } + + /** Provide the status of face authentication */ + val authenticationStatus = repository.authenticationStatus + + /** Provide the status of face detection */ + val detectionStatus = repository.detectionStatus + + private fun observeFaceAuthStateUpdates() { + authenticationStatus + .onEach { authStatusUpdate -> + listeners.forEach { it.onAuthenticationStatusChanged(authStatusUpdate) } + } + .flowOn(mainDispatcher) + .launchIn(applicationScope) + detectionStatus + .onEach { detectionStatusUpdate -> + listeners.forEach { it.onDetectionStatusChanged(detectionStatusUpdate) } + } + .flowOn(mainDispatcher) + .launchIn(applicationScope) + } + + companion object { + const val TAG = "KeyguardFaceAuthInteractor" + } +} + +/** + * Listener that can be registered with the [KeyguardFaceAuthInteractor] to receive updates about + * face authentication & detection updates. + * + * This is present to make it easier for use the new face auth API for code that cannot use + * [KeyguardFaceAuthInteractor.authenticationStatus] or [KeyguardFaceAuthInteractor.detectionStatus] + * flows. + */ +interface FaceAuthenticationListener { + /** Receive face authentication status updates */ + fun onAuthenticationStatusChanged(status: AuthenticationStatus) + + /** Receive status updates whenever face detection runs */ + fun onDetectionStatusChanged(status: DetectionStatus) +} + +// Extension method that filters a generic Boolean flow to one that emits +// whenever there is flip from false -> true +private fun Flow<Boolean>.whenItFlipsToTrue(): Flow<Boolean> { + return this.pairwise() + .filter { pair -> !pair.previousValue && pair.newValue } + .map { it.newValue } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index aabd212c1bd3..da0ada160518 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -78,6 +78,14 @@ constructor( val primaryBouncerToGoneTransition: Flow<TransitionStep> = repository.transition(PRIMARY_BOUNCER, GONE) + /** OFF->LOCKSCREEN transition information. */ + val offToLockscreenTransition: Flow<TransitionStep> = + repository.transition(KeyguardState.OFF, LOCKSCREEN) + + /** DOZING->LOCKSCREEN transition information. */ + val dozingToLockscreenTransition: Flow<TransitionStep> = + repository.transition(KeyguardState.DOZING, LOCKSCREEN) + /** * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <-> * Lockscreen (0f). diff --git a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt index f7355d5c11e2..7f6e4a903d76 100644 --- a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt @@ -4,6 +4,7 @@ import android.hardware.face.FaceManager import android.hardware.face.FaceSensorPropertiesInternal import com.android.keyguard.FaceAuthUiEvent import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.log.dagger.FaceAuthLog import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogLevel.DEBUG @@ -27,15 +28,15 @@ class FaceAuthenticationLogger constructor( @FaceAuthLog private val logBuffer: LogBuffer, ) { - fun ignoredFaceAuthTrigger(uiEvent: FaceAuthUiEvent) { + fun ignoredFaceAuthTrigger(uiEvent: FaceAuthUiEvent, ignoredReason: String) { logBuffer.log( TAG, DEBUG, - { str1 = uiEvent.reason }, { - "Ignoring trigger because face auth is currently running. " + - "Trigger reason: $str1" - } + str1 = uiEvent.reason + str2 = ignoredReason + }, + { "Ignoring trigger because $str2, Trigger reason: $str1" } ) } @@ -135,15 +136,6 @@ constructor( logBuffer.log(TAG, DEBUG, "Face authentication failed") } - fun authenticationAcquired(acquireInfo: Int) { - logBuffer.log( - TAG, - DEBUG, - { int1 = acquireInfo }, - { "Face acquired during face authentication: acquireInfo: $int1 " } - ) - } - fun authenticationError( errorCode: Int, errString: CharSequence?, @@ -217,4 +209,34 @@ constructor( fun cancellingFaceAuth() { logBuffer.log(TAG, DEBUG, "cancelling face auth because a gating condition became false") } + + fun interactorStarted() { + logBuffer.log(TAG, DEBUG, "KeyguardFaceAuthInteractor started") + } + + fun bouncerVisibilityChanged() { + logBuffer.log(TAG, DEBUG, "Triggering face auth because primary bouncer is visible") + } + + fun alternateBouncerVisibilityChanged() { + logBuffer.log(TAG, DEBUG, "Triggering face auth because alternate bouncer is visible") + } + + fun lockscreenBecameVisible(transitionStep: TransitionStep?) { + logBuffer.log( + TAG, + DEBUG, + { str1 = "$transitionStep" }, + { "Triggering face auth because lockscreen became visible due to transition: $str1" } + ) + } + + fun authRequested(uiEvent: FaceAuthUiEvent) { + logBuffer.log( + TAG, + DEBUG, + { str1 = "$uiEvent" }, + { "Requesting face auth for trigger: $str1" } + ) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt index 2489e043c7db..f21ea3dbed6d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt @@ -54,6 +54,7 @@ import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.log.FaceAuthenticationLogger import com.android.systemui.log.SessionTracker +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.phone.FakeKeyguardStateController import com.android.systemui.statusbar.phone.KeyguardBypassController @@ -61,8 +62,11 @@ import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.mockito.KotlinArgumentCaptor import com.android.systemui.util.mockito.captureMany import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.SystemClock import com.google.common.truth.Truth.assertThat +import java.io.PrintWriter +import java.io.StringWriter import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher @@ -88,8 +92,6 @@ import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.MockitoAnnotations -import java.io.PrintWriter -import java.io.StringWriter @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -183,8 +185,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { private fun createDeviceEntryFaceAuthRepositoryImpl( fmOverride: FaceManager? = faceManager, bypassControllerOverride: KeyguardBypassController? = bypassController - ) = - DeviceEntryFaceAuthRepositoryImpl( + ): DeviceEntryFaceAuthRepositoryImpl { + val systemClock = FakeSystemClock() + val faceAuthBuffer = TableLogBuffer(10, "face auth", systemClock) + val faceDetectBuffer = TableLogBuffer(10, "face detect", systemClock) + return DeviceEntryFaceAuthRepositoryImpl( mContext, fmOverride, fakeUserRepository, @@ -200,8 +205,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { keyguardRepository, keyguardInteractor, alternateBouncerInteractor, + faceDetectBuffer, + faceAuthBuffer, dumpManager, ) + } @Test fun faceAuthRunsAndProvidesAuthStatusUpdates() = diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt new file mode 100644 index 000000000000..e0acf55ddac1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.os.Handler +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.keyguard.FaceAuthUiEvent +import com.android.keyguard.KeyguardSecurityModel +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.SysuiTestCase +import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.dump.logcatLogBuffer +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.DismissCallbackRegistry +import com.android.systemui.keyguard.data.BouncerView +import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository +import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.log.FaceAuthenticationLogger +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.phone.KeyguardBypassController +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestCoroutineScheduler +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.Mock +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class KeyguardFaceAuthInteractorTest : SysuiTestCase() { + + private lateinit var underTest: KeyguardFaceAuthInteractor + private lateinit var testScope: TestScope + private lateinit var bouncerRepository: FakeKeyguardBouncerRepository + private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository + private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor + private lateinit var faceAuthRepository: FakeDeviceEntryFaceAuthRepository + + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + val scheduler = TestCoroutineScheduler() + val dispatcher = StandardTestDispatcher(scheduler) + testScope = TestScope(dispatcher) + val featureFlags = FakeFeatureFlags() + featureFlags.set(Flags.FACE_AUTH_REFACTOR, true) + bouncerRepository = FakeKeyguardBouncerRepository() + faceAuthRepository = FakeDeviceEntryFaceAuthRepository() + keyguardTransitionRepository = FakeKeyguardTransitionRepository() + keyguardTransitionInteractor = KeyguardTransitionInteractor(keyguardTransitionRepository) + + underTest = + KeyguardFaceAuthInteractor( + testScope.backgroundScope, + dispatcher, + faceAuthRepository, + PrimaryBouncerInteractor( + bouncerRepository, + mock(BouncerView::class.java), + mock(Handler::class.java), + mock(KeyguardStateController::class.java), + mock(KeyguardSecurityModel::class.java), + mock(PrimaryBouncerCallbackInteractor::class.java), + mock(FalsingCollector::class.java), + mock(DismissCallbackRegistry::class.java), + context, + keyguardUpdateMonitor, + mock(KeyguardBypassController::class.java), + ), + AlternateBouncerInteractor( + mock(StatusBarStateController::class.java), + mock(KeyguardStateController::class.java), + bouncerRepository, + mock(BiometricSettingsRepository::class.java), + FakeDeviceEntryFingerprintAuthRepository(), + FakeSystemClock(), + ), + keyguardTransitionInteractor, + featureFlags, + FaceAuthenticationLogger(logcatLogBuffer("faceAuthBuffer")), + keyguardUpdateMonitor, + ) + } + + @Test + fun faceAuthIsRequestedWhenLockscreenBecomesVisibleFromOffState() = + testScope.runTest { + underTest.start() + + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + KeyguardState.OFF, + KeyguardState.LOCKSCREEN, + transitionState = TransitionState.STARTED + ) + ) + + runCurrent() + assertThat(faceAuthRepository.runningAuthRequest.value) + .isEqualTo( + Pair(FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED, true) + ) + } + + @Test + fun faceAuthIsRequestedWhenLockscreenBecomesVisibleFromAodState() = + testScope.runTest { + underTest.start() + + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + KeyguardState.AOD, + KeyguardState.LOCKSCREEN, + transitionState = TransitionState.STARTED + ) + ) + + runCurrent() + assertThat(faceAuthRepository.runningAuthRequest.value) + .isEqualTo( + Pair(FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED, true) + ) + } + + @Test + fun faceAuthIsRequestedWhenLockscreenBecomesVisibleFromDozingState() = + testScope.runTest { + underTest.start() + + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + KeyguardState.DOZING, + KeyguardState.LOCKSCREEN, + transitionState = TransitionState.STARTED + ) + ) + + runCurrent() + assertThat(faceAuthRepository.runningAuthRequest.value) + .isEqualTo( + Pair(FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED, true) + ) + } + + @Test + fun faceAuthIsRequestedWhenPrimaryBouncerIsVisible() = + testScope.runTest { + underTest.start() + + bouncerRepository.setPrimaryShow(false) + runCurrent() + + bouncerRepository.setPrimaryShow(true) + + runCurrent() + assertThat(faceAuthRepository.runningAuthRequest.value) + .isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN, true)) + } + + @Test + fun faceAuthIsRequestedWhenAlternateBouncerIsVisible() = + testScope.runTest { + underTest.start() + + bouncerRepository.setAlternateVisible(false) + runCurrent() + + bouncerRepository.setAlternateVisible(true) + + runCurrent() + assertThat(faceAuthRepository.runningAuthRequest.value) + .isEqualTo( + Pair( + FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN, + false + ) + ) + } + + @Test + fun faceAuthIsRequestedWhenUdfpsSensorTouched() = + testScope.runTest { + underTest.start() + + underTest.onUdfpsSensorTouched() + + runCurrent() + assertThat(faceAuthRepository.runningAuthRequest.value) + .isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_UDFPS_POINTER_DOWN, false)) + } + + @Test + fun faceAuthIsRequestedWhenOnAssistantTriggeredOnLockScreen() = + testScope.runTest { + underTest.start() + + underTest.onAssistantTriggeredOnLockScreen() + + runCurrent() + assertThat(faceAuthRepository.runningAuthRequest.value) + .isEqualTo( + Pair(FaceAuthUiEvent.FACE_AUTH_UPDATED_ASSISTANT_VISIBILITY_CHANGED, true) + ) + } + + @Test + fun faceAuthIsRequestedWhenDeviceLifted() = + testScope.runTest { + underTest.start() + + underTest.onDeviceLifted() + + runCurrent() + assertThat(faceAuthRepository.runningAuthRequest.value) + .isEqualTo( + Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED, true) + ) + } + + @Test + fun faceAuthIsRequestedWhenQsExpansionStared() = + testScope.runTest { + underTest.start() + + underTest.onQsExpansionStared() + + runCurrent() + assertThat(faceAuthRepository.runningAuthRequest.value) + .isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED, true)) + } + + @Test + fun faceAuthIsRequestedWhenNotificationPanelClicked() = + testScope.runTest { + underTest.start() + + underTest.onNotificationPanelClicked() + + runCurrent() + assertThat(faceAuthRepository.runningAuthRequest.value) + .isEqualTo( + Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED, true) + ) + } + + @Test + fun faceAuthIsRequestedWhenSwipeUpOnBouncer() = + testScope.runTest { + underTest.start() + + underTest.onSwipeUpOnBouncer() + + runCurrent() + assertThat(faceAuthRepository.runningAuthRequest.value) + .isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, false)) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt index c08ecd0e3b0c..738f09ddce3d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt @@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.map class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository { @@ -46,14 +45,19 @@ class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository { private val _runningAuthRequest = MutableStateFlow<Pair<FaceAuthUiEvent, Boolean>?>(null) val runningAuthRequest: StateFlow<Pair<FaceAuthUiEvent, Boolean>?> = _runningAuthRequest.asStateFlow() - override val isAuthRunning = _runningAuthRequest.map { it != null } + + private val _isAuthRunning = MutableStateFlow(false) + override val isAuthRunning: StateFlow<Boolean> = _isAuthRunning + override val isBypassEnabled = MutableStateFlow(false) override suspend fun authenticate(uiEvent: FaceAuthUiEvent, fallbackToDetection: Boolean) { _runningAuthRequest.value = uiEvent to fallbackToDetection + _isAuthRunning.value = true } override fun cancel() { + _isAuthRunning.value = false _runningAuthRequest.value = null } } |