diff --git a/core/java/android/hardware/face/FaceManager.java b/core/java/android/hardware/face/FaceManager.java index 55b20e17d4d79..2f30075125dc1 100644 --- a/core/java/android/hardware/face/FaceManager.java +++ b/core/java/android/hardware/face/FaceManager.java @@ -44,6 +44,7 @@ import android.os.PowerManager; import android.os.RemoteException; import android.os.Trace; import android.os.UserHandle; +import android.provider.Settings; import android.util.Slog; import android.view.Surface; @@ -127,6 +128,11 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan @Override // binder call public void onRemoved(Face face, int remaining) { mHandler.obtainMessage(MSG_REMOVED, remaining, 0, face).sendToTarget(); + if (remaining == 0) { + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.FACE_UNLOCK_RE_ENROLL, 0, + UserHandle.USER_CURRENT); + } } @Override diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java index eb8136e39d299..84de1a6043ab1 100644 --- a/core/java/android/hardware/fingerprint/FingerprintManager.java +++ b/core/java/android/hardware/fingerprint/FingerprintManager.java @@ -832,7 +832,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing } /** - * Removes all face templates for the given user. + * Removes all fingerprint templates for the given user. * @hide */ @RequiresPermission(MANAGE_FINGERPRINT) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 7cb959dee245e..dae2ae654a7d2 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -10178,9 +10178,7 @@ public final class Settings { * * Face unlock re enroll. * 0 = No re enrollment. - * 1 = Re enrollment is suggested. - * 2 = Re enrollment is required after a set time period. - * 3 = Re enrollment is required immediately. + * 1 = Re enrollment is required. * * @hide */ diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 74ae954a539c8..b9943a8e43924 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -404,7 +404,34 @@ If you enter an incorrect PIN on the next attempt, your work profile and its data will be deleted. If you enter an incorrect password on the next attempt, your work profile and its data will be deleted. - + + Set up + + Not now + + This is required to improve security and performance + + Set up Fingerprint Unlock again + + Fingerprint Unlock + + Set up Fingerprint Unlock + + To set up Fingerprint Unlock again, your current fingerprint images and models will be deleted.\n\nAfter they\’re deleted, you\’ll need to set up Fingerprint Unlock again to use your fingerprint to unlock your phone or verify it\’s you. + + To set up Fingerprint Unlock again, your current fingerprint images and model will be deleted.\n\nAfter they\’re deleted, you\’ll need to set up Fingerprint Unlock again to use your fingerprint to unlock your phone or verify it\’s you. + + Couldn\u2019t set up fingerprint unlock. Go to Settings to try again. + + Set up Face Unlock again + + Face Unlock + + Set up Face Unlock + + To set up Face Unlock again, your current face model will be deleted.\n\nYou\’ll need to set up this feature again to use your face to unlock your phone. + + Couldn\u2019t set up face unlock. Go to Settings to try again. Touch the fingerprint sensor diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationBroadcastReceiver.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationBroadcastReceiver.java new file mode 100644 index 0000000000000..c22a66b210cbd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationBroadcastReceiver.java @@ -0,0 +1,72 @@ +/* + * 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.biometrics; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.hardware.biometrics.BiometricSourceType; + +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.statusbar.phone.SystemUIDialog; + +import javax.inject.Inject; + +/** + * Receives broadcasts sent by {@link BiometricNotificationService} and takes + * the appropriate action. + */ +@SysUISingleton +public class BiometricNotificationBroadcastReceiver extends BroadcastReceiver { + static final String ACTION_SHOW_FACE_REENROLL_DIALOG = "face_action_show_reenroll_dialog"; + static final String ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG = + "fingerprint_action_show_reenroll_dialog"; + + private static final String TAG = "BiometricNotificationBroadcastReceiver"; + + private final Context mContext; + private final BiometricNotificationDialogFactory mNotificationDialogFactory; + @Inject + BiometricNotificationBroadcastReceiver(Context context, + BiometricNotificationDialogFactory notificationDialogFactory) { + mContext = context; + mNotificationDialogFactory = notificationDialogFactory; + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + + switch (action) { + case ACTION_SHOW_FACE_REENROLL_DIALOG: + mNotificationDialogFactory.createReenrollDialog(mContext, + new SystemUIDialog(mContext), + BiometricSourceType.FACE) + .show(); + break; + case ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG: + mNotificationDialogFactory.createReenrollDialog( + mContext, + new SystemUIDialog(mContext), + BiometricSourceType.FINGERPRINT) + .show(); + break; + default: + break; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationDialogFactory.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationDialogFactory.java new file mode 100644 index 0000000000000..3e6508c6da706 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationDialogFactory.java @@ -0,0 +1,177 @@ +/* + * 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.biometrics; + +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.hardware.biometrics.BiometricSourceType; +import android.hardware.face.Face; +import android.hardware.face.FaceManager; +import android.hardware.fingerprint.Fingerprint; +import android.hardware.fingerprint.FingerprintManager; +import android.provider.Settings; +import android.util.Log; + +import com.android.systemui.R; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.statusbar.phone.SystemUIDialog; + +import javax.inject.Inject; + +/** + * Manages the creation of dialogs to be shown for biometric re enroll notifications. + */ +@SysUISingleton +public class BiometricNotificationDialogFactory { + private static final String TAG = "BiometricNotificationDialogFactory"; + + @Inject + BiometricNotificationDialogFactory() {} + + Dialog createReenrollDialog(final Context context, final SystemUIDialog sysuiDialog, + BiometricSourceType biometricSourceType) { + if (biometricSourceType == BiometricSourceType.FACE) { + sysuiDialog.setTitle(context.getString(R.string.face_re_enroll_dialog_title)); + sysuiDialog.setMessage(context.getString(R.string.face_re_enroll_dialog_content)); + } else if (biometricSourceType == BiometricSourceType.FINGERPRINT) { + FingerprintManager fingerprintManager = context.getSystemService( + FingerprintManager.class); + sysuiDialog.setTitle(context.getString(R.string.fingerprint_re_enroll_dialog_title)); + if (fingerprintManager.getEnrolledFingerprints().size() == 1) { + sysuiDialog.setMessage(context.getString( + R.string.fingerprint_re_enroll_dialog_content_singular)); + } else { + sysuiDialog.setMessage(context.getString( + R.string.fingerprint_re_enroll_dialog_content)); + } + } + + sysuiDialog.setPositiveButton(R.string.biometric_re_enroll_dialog_confirm, + (dialog, which) -> onReenrollDialogConfirm(context, biometricSourceType)); + sysuiDialog.setNegativeButton(R.string.biometric_re_enroll_dialog_cancel, + (dialog, which) -> {}); + return sysuiDialog; + } + + private static Dialog createReenrollFailureDialog(Context context, + BiometricSourceType biometricType) { + final SystemUIDialog sysuiDialog = new SystemUIDialog(context); + + if (biometricType == BiometricSourceType.FACE) { + sysuiDialog.setMessage(context.getString( + R.string.face_reenroll_failure_dialog_content)); + } else if (biometricType == BiometricSourceType.FINGERPRINT) { + sysuiDialog.setMessage(context.getString( + R.string.fingerprint_reenroll_failure_dialog_content)); + } + + sysuiDialog.setPositiveButton(R.string.ok, (dialog, which) -> {}); + return sysuiDialog; + } + + private static void onReenrollDialogConfirm(final Context context, + BiometricSourceType biometricType) { + if (biometricType == BiometricSourceType.FACE) { + reenrollFace(context); + } else if (biometricType == BiometricSourceType.FINGERPRINT) { + reenrollFingerprint(context); + } + } + + private static void reenrollFingerprint(Context context) { + FingerprintManager fingerprintManager = context.getSystemService(FingerprintManager.class); + if (fingerprintManager == null) { + Log.e(TAG, "Not launching enrollment. Fingerprint manager was null!"); + createReenrollFailureDialog(context, BiometricSourceType.FINGERPRINT).show(); + return; + } + + if (!fingerprintManager.hasEnrolledTemplates(context.getUserId())) { + createReenrollFailureDialog(context, BiometricSourceType.FINGERPRINT).show(); + return; + } + + // Remove all enrolled fingerprint. Launch enrollment if successful. + fingerprintManager.removeAll(context.getUserId(), + new FingerprintManager.RemovalCallback() { + boolean mDidShowFailureDialog; + + @Override + public void onRemovalError(Fingerprint fingerprint, int errMsgId, + CharSequence errString) { + Log.e(TAG, "Not launching enrollment." + + "Failed to remove existing face(s)."); + if (!mDidShowFailureDialog) { + mDidShowFailureDialog = true; + createReenrollFailureDialog(context, BiometricSourceType.FINGERPRINT) + .show(); + } + } + + @Override + public void onRemovalSucceeded(Fingerprint fingerprint, int remaining) { + if (!mDidShowFailureDialog && remaining == 0) { + Intent intent = new Intent(Settings.ACTION_FINGERPRINT_ENROLL); + intent.setPackage("com.android.settings"); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + } + }); + } + + private static void reenrollFace(Context context) { + FaceManager faceManager = context.getSystemService(FaceManager.class); + if (faceManager == null) { + Log.e(TAG, "Not launching enrollment. Face manager was null!"); + createReenrollFailureDialog(context, BiometricSourceType.FACE).show(); + return; + } + + if (!faceManager.hasEnrolledTemplates(context.getUserId())) { + createReenrollFailureDialog(context, BiometricSourceType.FACE).show(); + return; + } + + // Remove all enrolled faces. Launch enrollment if successful. + faceManager.removeAll(context.getUserId(), + new FaceManager.RemovalCallback() { + boolean mDidShowFailureDialog; + + @Override + public void onRemovalError(Face face, int errMsgId, CharSequence errString) { + Log.e(TAG, "Not launching enrollment." + + "Failed to remove existing face(s)."); + if (!mDidShowFailureDialog) { + mDidShowFailureDialog = true; + createReenrollFailureDialog(context, BiometricSourceType.FACE).show(); + } + } + + @Override + public void onRemovalSucceeded(Face face, int remaining) { + if (!mDidShowFailureDialog && remaining == 0) { + Intent intent = new Intent("android.settings.FACE_ENROLL"); + intent.setPackage("com.android.settings"); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + } + }); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java new file mode 100644 index 0000000000000..4b17be3c45d4c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java @@ -0,0 +1,206 @@ +/* + * 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.biometrics; + +import static android.app.PendingIntent.FLAG_IMMUTABLE; + +import static com.android.systemui.biometrics.BiometricNotificationBroadcastReceiver.ACTION_SHOW_FACE_REENROLL_DIALOG; +import static com.android.systemui.biometrics.BiometricNotificationBroadcastReceiver.ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.biometrics.BiometricFaceConstants; +import android.hardware.biometrics.BiometricFingerprintConstants; +import android.hardware.biometrics.BiometricSourceType; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Log; + +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.systemui.CoreStartable; +import com.android.systemui.R; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.statusbar.policy.KeyguardStateController; + +import javax.inject.Inject; + +/** + * Handles showing system notifications related to biometric unlock. + */ +@SysUISingleton +public class BiometricNotificationService implements CoreStartable { + + private static final String TAG = "BiometricNotificationService"; + private static final String CHANNEL_ID = "BiometricHiPriNotificationChannel"; + private static final String CHANNEL_NAME = " Biometric Unlock"; + private static final int FACE_NOTIFICATION_ID = 1; + private static final int FINGERPRINT_NOTIFICATION_ID = 2; + private static final long SHOW_NOTIFICATION_DELAY_MS = 5_000L; // 5 seconds + private static final int REENROLL_REQUIRED = 1; + private static final int REENROLL_NOT_REQUIRED = 0; + + private final Context mContext; + private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + private final KeyguardStateController mKeyguardStateController; + private final Handler mHandler; + private final NotificationManager mNotificationManager; + private final BiometricNotificationBroadcastReceiver mBroadcastReceiver; + private NotificationChannel mNotificationChannel; + private boolean mFaceNotificationQueued; + private boolean mFingerprintNotificationQueued; + private boolean mFingerprintReenrollRequired; + + private final KeyguardStateController.Callback mKeyguardStateControllerCallback = + new KeyguardStateController.Callback() { + private boolean mIsShowing = true; + @Override + public void onKeyguardShowingChanged() { + if (mKeyguardStateController.isShowing() + || mKeyguardStateController.isShowing() == mIsShowing) { + mIsShowing = mKeyguardStateController.isShowing(); + return; + } + mIsShowing = mKeyguardStateController.isShowing(); + if (isFaceReenrollRequired(mContext) && !mFaceNotificationQueued) { + queueFaceReenrollNotification(); + } + if (mFingerprintReenrollRequired && !mFingerprintNotificationQueued) { + mFingerprintReenrollRequired = false; + queueFingerprintReenrollNotification(); + } + } + }; + + private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback = + new KeyguardUpdateMonitorCallback() { + @Override + public void onBiometricError(int msgId, String errString, + BiometricSourceType biometricSourceType) { + if (msgId == BiometricFaceConstants.BIOMETRIC_ERROR_RE_ENROLL + && biometricSourceType == BiometricSourceType.FACE) { + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.FACE_UNLOCK_RE_ENROLL, REENROLL_REQUIRED, + UserHandle.USER_CURRENT); + } else if (msgId == BiometricFingerprintConstants.BIOMETRIC_ERROR_RE_ENROLL + && biometricSourceType == BiometricSourceType.FINGERPRINT) { + mFingerprintReenrollRequired = true; + } + } + }; + + + @Inject + public BiometricNotificationService(Context context, + KeyguardUpdateMonitor keyguardUpdateMonitor, + KeyguardStateController keyguardStateController, + Handler handler, NotificationManager notificationManager, + BiometricNotificationBroadcastReceiver biometricNotificationBroadcastReceiver) { + mContext = context; + mKeyguardUpdateMonitor = keyguardUpdateMonitor; + mKeyguardStateController = keyguardStateController; + mHandler = handler; + mNotificationManager = notificationManager; + mBroadcastReceiver = biometricNotificationBroadcastReceiver; + } + + @Override + public void start() { + mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback); + mKeyguardStateController.addCallback(mKeyguardStateControllerCallback); + mNotificationChannel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, + NotificationManager.IMPORTANCE_HIGH); + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG); + intentFilter.addAction(ACTION_SHOW_FACE_REENROLL_DIALOG); + mContext.registerReceiver(mBroadcastReceiver, intentFilter, + Context.RECEIVER_EXPORTED_UNAUDITED); + } + + private void queueFaceReenrollNotification() { + mFaceNotificationQueued = true; + final String title = mContext.getString(R.string.face_re_enroll_notification_title); + final String content = mContext.getString( + R.string.biometric_re_enroll_notification_content); + final String name = mContext.getString(R.string.face_re_enroll_notification_name); + mHandler.postDelayed( + () -> showNotification(ACTION_SHOW_FACE_REENROLL_DIALOG, title, content, name, + FACE_NOTIFICATION_ID), + SHOW_NOTIFICATION_DELAY_MS); + } + + private void queueFingerprintReenrollNotification() { + mFingerprintNotificationQueued = true; + final String title = mContext.getString(R.string.fingerprint_re_enroll_notification_title); + final String content = mContext.getString( + R.string.biometric_re_enroll_notification_content); + final String name = mContext.getString(R.string.fingerprint_re_enroll_notification_name); + mHandler.postDelayed( + () -> showNotification(ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG, title, content, + name, FINGERPRINT_NOTIFICATION_ID), + SHOW_NOTIFICATION_DELAY_MS); + } + + private void showNotification(String action, CharSequence title, CharSequence content, + CharSequence name, int notificationId) { + if (notificationId == FACE_NOTIFICATION_ID) { + mFaceNotificationQueued = false; + } else if (notificationId == FINGERPRINT_NOTIFICATION_ID) { + mFingerprintNotificationQueued = false; + } + + if (mNotificationManager == null) { + Log.e(TAG, "Failed to show notification " + + action + ". Notification manager is null!"); + return; + } + + final Intent onClickIntent = new Intent(action); + final PendingIntent onClickPendingIntent = PendingIntent.getBroadcastAsUser(mContext, + 0 /* requestCode */, onClickIntent, FLAG_IMMUTABLE, UserHandle.CURRENT); + + final Notification notification = new Notification.Builder(mContext, CHANNEL_ID) + .setCategory(Notification.CATEGORY_SYSTEM) + .setSmallIcon(com.android.internal.R.drawable.ic_lock) + .setContentTitle(title) + .setContentText(content) + .setSubText(name) + .setContentIntent(onClickPendingIntent) + .setAutoCancel(true) + .setLocalOnly(true) + .setOnlyAlertOnce(true) + .setVisibility(Notification.VISIBILITY_SECRET) + .build(); + + mNotificationManager.createNotificationChannel(mNotificationChannel); + mNotificationManager.notifyAsUser(TAG, notificationId, notification, UserHandle.CURRENT); + } + + private boolean isFaceReenrollRequired(Context context) { + final int settingValue = + Settings.Secure.getIntForUser(context.getContentResolver(), + Settings.Secure.FACE_UNLOCK_RE_ENROLL, REENROLL_NOT_REQUIRED, + UserHandle.USER_CURRENT); + return settingValue == REENROLL_REQUIRED; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 9bf6b2a5b42b4..1b144023622c4 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -25,6 +25,7 @@ import com.android.systemui.SliceBroadcastRelayHandler import com.android.systemui.accessibility.SystemActions import com.android.systemui.accessibility.WindowMagnification import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.BiometricNotificationService import com.android.systemui.clipboardoverlay.ClipboardListener import com.android.systemui.controls.dagger.StartControlsStartableModule import com.android.systemui.dagger.qualifiers.PerUser @@ -75,6 +76,14 @@ abstract class SystemUICoreStartableModule { @ClassKey(AuthController::class) abstract fun bindAuthController(service: AuthController): CoreStartable + /** Inject into BiometricNotificationService */ + @Binds + @IntoMap + @ClassKey(BiometricNotificationService::class) + abstract fun bindBiometricNotificationService( + service: BiometricNotificationService + ): CoreStartable + /** Inject into ChooserCoreStartable. */ @Binds @IntoMap diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java new file mode 100644 index 0000000000000..362d26b040e87 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java @@ -0,0 +1,174 @@ +/* + * 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.biometrics; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.hardware.biometrics.BiometricSourceType; +import android.hardware.face.FaceManager; +import android.hardware.fingerprint.FingerprintManager; +import android.provider.Settings; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.phone.SystemUIDialog; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.concurrent.ExecutionException; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class BiometricNotificationDialogFactoryTest extends SysuiTestCase { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Mock + FingerprintManager mFingerprintManager; + @Mock + FaceManager mFaceManager; + @Mock + SystemUIDialog mDialog; + + private Context mContextSpy; + private final ArgumentCaptor mOnClickListenerArgumentCaptor = + ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + private final ArgumentCaptor mIntentArgumentCaptor = + ArgumentCaptor.forClass(Intent.class); + private BiometricNotificationDialogFactory mDialogFactory; + + @Before + public void setUp() throws ExecutionException, InterruptedException { + mContext.addMockSystemService(FingerprintManager.class, mFingerprintManager); + mContext.addMockSystemService(FaceManager.class, mFaceManager); + + when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + + mContextSpy = spy(mContext); + mDialogFactory = new BiometricNotificationDialogFactory(); + } + + @Test + public void testFingerprintReEnrollDialog_onRemovalSucceeded() { + mDialogFactory.createReenrollDialog(mContextSpy, mDialog, + BiometricSourceType.FINGERPRINT); + + verify(mDialog).setPositiveButton(anyInt(), mOnClickListenerArgumentCaptor.capture()); + + DialogInterface.OnClickListener positiveOnClickListener = + mOnClickListenerArgumentCaptor.getValue(); + positiveOnClickListener.onClick(null, DialogInterface.BUTTON_POSITIVE); + ArgumentCaptor removalCallbackArgumentCaptor = + ArgumentCaptor.forClass(FingerprintManager.RemovalCallback.class); + + verify(mFingerprintManager).removeAll(anyInt(), removalCallbackArgumentCaptor.capture()); + + removalCallbackArgumentCaptor.getValue().onRemovalSucceeded(null /* fp */, + 0 /* remaining */); + + verify(mContextSpy).startActivity(mIntentArgumentCaptor.capture()); + assertThat(mIntentArgumentCaptor.getValue().getAction()).isEqualTo( + Settings.ACTION_FINGERPRINT_ENROLL); + } + + @Test + public void testFingerprintReEnrollDialog_onRemovalError() { + mDialogFactory.createReenrollDialog(mContextSpy, mDialog, + BiometricSourceType.FINGERPRINT); + + verify(mDialog).setPositiveButton(anyInt(), mOnClickListenerArgumentCaptor.capture()); + + DialogInterface.OnClickListener positiveOnClickListener = + mOnClickListenerArgumentCaptor.getValue(); + positiveOnClickListener.onClick(null, DialogInterface.BUTTON_POSITIVE); + ArgumentCaptor removalCallbackArgumentCaptor = + ArgumentCaptor.forClass(FingerprintManager.RemovalCallback.class); + + verify(mFingerprintManager).removeAll(anyInt(), removalCallbackArgumentCaptor.capture()); + + removalCallbackArgumentCaptor.getValue().onRemovalError(null /* fp */, + 0 /* errmsgId */, "Error" /* errString */); + + verify(mContextSpy, never()).startActivity(any()); + } + + @Test + public void testFaceReEnrollDialog_onRemovalSucceeded() { + mDialogFactory.createReenrollDialog(mContextSpy, mDialog, + BiometricSourceType.FACE); + + verify(mDialog).setPositiveButton(anyInt(), mOnClickListenerArgumentCaptor.capture()); + + DialogInterface.OnClickListener positiveOnClickListener = + mOnClickListenerArgumentCaptor.getValue(); + positiveOnClickListener.onClick(null, DialogInterface.BUTTON_POSITIVE); + ArgumentCaptor removalCallbackArgumentCaptor = + ArgumentCaptor.forClass(FaceManager.RemovalCallback.class); + + verify(mFaceManager).removeAll(anyInt(), removalCallbackArgumentCaptor.capture()); + + removalCallbackArgumentCaptor.getValue().onRemovalSucceeded(null /* fp */, + 0 /* remaining */); + + verify(mContextSpy).startActivity(mIntentArgumentCaptor.capture()); + assertThat(mIntentArgumentCaptor.getValue().getAction()).isEqualTo( + "android.settings.FACE_ENROLL"); + } + + @Test + public void testFaceReEnrollDialog_onRemovalError() { + mDialogFactory.createReenrollDialog(mContextSpy, mDialog, + BiometricSourceType.FACE); + + verify(mDialog).setPositiveButton(anyInt(), mOnClickListenerArgumentCaptor.capture()); + + DialogInterface.OnClickListener positiveOnClickListener = + mOnClickListenerArgumentCaptor.getValue(); + positiveOnClickListener.onClick(null, DialogInterface.BUTTON_POSITIVE); + ArgumentCaptor removalCallbackArgumentCaptor = + ArgumentCaptor.forClass(FaceManager.RemovalCallback.class); + + verify(mFaceManager).removeAll(anyInt(), removalCallbackArgumentCaptor.capture()); + + removalCallbackArgumentCaptor.getValue().onRemovalError(null /* face */, + 0 /* errmsgId */, "Error" /* errString */); + + verify(mContextSpy, never()).startActivity(any()); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java new file mode 100644 index 0000000000000..b8bca3a403e10 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java @@ -0,0 +1,152 @@ +/* + * 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.biometrics; + +import static com.android.systemui.biometrics.BiometricNotificationBroadcastReceiver.ACTION_SHOW_FACE_REENROLL_DIALOG; +import static com.android.systemui.biometrics.BiometricNotificationBroadcastReceiver.ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Notification; +import android.app.NotificationManager; +import android.hardware.biometrics.BiometricFaceConstants; +import android.hardware.biometrics.BiometricFingerprintConstants; +import android.hardware.biometrics.BiometricSourceType; +import android.os.Handler; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.policy.KeyguardStateController; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class BiometricNotificationServiceTest extends SysuiTestCase { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Mock + KeyguardUpdateMonitor mKeyguardUpdateMonitor; + @Mock + KeyguardStateController mKeyguardStateController; + @Mock + NotificationManager mNotificationManager; + + private static final String TAG = "BiometricNotificationService"; + private static final int FACE_NOTIFICATION_ID = 1; + private static final int FINGERPRINT_NOTIFICATION_ID = 2; + private static final long SHOW_NOTIFICATION_DELAY_MS = 5_000L; // 5 seconds + + private final ArgumentCaptor mNotificationArgumentCaptor = + ArgumentCaptor.forClass(Notification.class); + private TestableLooper mLooper; + private KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback; + private KeyguardStateController.Callback mKeyguardStateControllerCallback; + + @Before + public void setUp() { + mLooper = TestableLooper.get(this); + Handler handler = new Handler(mLooper.getLooper()); + BiometricNotificationDialogFactory dialogFactory = new BiometricNotificationDialogFactory(); + BiometricNotificationBroadcastReceiver broadcastReceiver = + new BiometricNotificationBroadcastReceiver(mContext, dialogFactory); + BiometricNotificationService biometricNotificationService = + new BiometricNotificationService(mContext, + mKeyguardUpdateMonitor, mKeyguardStateController, handler, + mNotificationManager, + broadcastReceiver); + biometricNotificationService.start(); + + ArgumentCaptor updateMonitorCallbackArgumentCaptor = + ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); + ArgumentCaptor stateControllerCallbackArgumentCaptor = + ArgumentCaptor.forClass(KeyguardStateController.Callback.class); + + verify(mKeyguardUpdateMonitor).registerCallback( + updateMonitorCallbackArgumentCaptor.capture()); + verify(mKeyguardStateController).addCallback( + stateControllerCallbackArgumentCaptor.capture()); + + mKeyguardUpdateMonitorCallback = updateMonitorCallbackArgumentCaptor.getValue(); + mKeyguardStateControllerCallback = stateControllerCallbackArgumentCaptor.getValue(); + } + + @Test + public void testShowFingerprintReEnrollNotification() { + when(mKeyguardStateController.isShowing()).thenReturn(false); + + mKeyguardUpdateMonitorCallback.onBiometricError( + BiometricFingerprintConstants.BIOMETRIC_ERROR_RE_ENROLL, + "Testing Fingerprint Re-enrollment" /* errString */, + BiometricSourceType.FINGERPRINT + ); + mKeyguardStateControllerCallback.onKeyguardShowingChanged(); + + mLooper.moveTimeForward(SHOW_NOTIFICATION_DELAY_MS); + mLooper.processAllMessages(); + + verify(mNotificationManager).notifyAsUser(eq(TAG), eq(FINGERPRINT_NOTIFICATION_ID), + mNotificationArgumentCaptor.capture(), any()); + + Notification fingerprintNotification = mNotificationArgumentCaptor.getValue(); + + assertThat(fingerprintNotification.contentIntent.getIntent().getAction()) + .isEqualTo(ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG); + } + @Test + public void testShowFaceReEnrollNotification() { + when(mKeyguardStateController.isShowing()).thenReturn(false); + + mKeyguardUpdateMonitorCallback.onBiometricError( + BiometricFaceConstants.BIOMETRIC_ERROR_RE_ENROLL, + "Testing Face Re-enrollment" /* errString */, + BiometricSourceType.FACE + ); + mKeyguardStateControllerCallback.onKeyguardShowingChanged(); + + mLooper.moveTimeForward(SHOW_NOTIFICATION_DELAY_MS); + mLooper.processAllMessages(); + + verify(mNotificationManager).notifyAsUser(eq(TAG), eq(FACE_NOTIFICATION_ID), + mNotificationArgumentCaptor.capture(), any()); + + Notification fingerprintNotification = mNotificationArgumentCaptor.getValue(); + + assertThat(fingerprintNotification.contentIntent.getIntent().getAction()) + .isEqualTo(ACTION_SHOW_FACE_REENROLL_DIALOG); + } + +}