Face and fingerprint re-enrollment notification

Bug: 258901849
Test: Manual: adb shell settings put secure face_unlock_re_enroll 1, then unlock through keyguard, the notification should occur in 5 seconds of unlocking
Test: atest BiometricNotificationServiceTest
Test: atest BiometricNotificationDialogFactoryTest

Change-Id: Ia9af235b6eefafd8508a42338aef39baf4f5c7ee
Merged-In: Ia9af235b6eefafd8508a42338aef39baf4f5c7ee
This commit is contained in:
Diya Bera
2023-04-13 20:20:29 +00:00
parent 271319b11e
commit cc14b18f5e
10 changed files with 826 additions and 5 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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
*/

View File

@@ -404,7 +404,34 @@
<string name="biometric_dialog_last_pin_attempt_before_wipe_profile">If you enter an incorrect PIN on the next attempt, your work profile and its data will be deleted.</string>
<!-- Content of a dialog shown when the user only has one attempt left to provide the correct password before the work profile is removed. [CHAR LIMIT=NONE] -->
<string name="biometric_dialog_last_password_attempt_before_wipe_profile">If you enter an incorrect password on the next attempt, your work profile and its data will be deleted.</string>
<!-- Confirmation button label for a dialog shown when the system requires the user to re-enroll their biometrics. [CHAR LIMIT=20] -->
<string name="biometric_re_enroll_dialog_confirm">Set up</string>
<!-- Cancel button label for a dialog shown when the system requires the user to re-enroll their biometric. [CHAR LIMIT=20] -->
<string name="biometric_re_enroll_dialog_cancel">Not now</string>
<!-- Notification content shown when the system requires the user to re-enroll their biometrics. [CHAR LIMIT=NONE] -->
<string name="biometric_re_enroll_notification_content">This is required to improve security and performance</string>
<!-- Notification title shown when the system requires the user to re-enroll their fingerprint. [CHAR LIMIT=NONE] -->
<string name="fingerprint_re_enroll_notification_title">Set up Fingerprint Unlock again</string>
<!-- Name shown for system notifications related to the fingerprint unlock feature. [CHAR LIMIT=NONE] -->
<string name="fingerprint_re_enroll_notification_name">Fingerprint Unlock</string>
<!-- Title for a dialog shown when the system requires the user to re-enroll their fingerprint. [CHAR LIMIT=NONE] -->
<string name="fingerprint_re_enroll_dialog_title">Set up Fingerprint Unlock</string>
<!-- Content for a dialog shown when the system requires the user to re-enroll their fingerprint. [CHAR LIMIT=NONE] -->
<string name="fingerprint_re_enroll_dialog_content">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.</string>
<!-- Content for a dialog shown when the system requires the user to re-enroll their fingerprint (singular). [CHAR LIMIT=NONE] -->
<string name="fingerprint_re_enroll_dialog_content_singular">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.</string>
<!-- Content for a dialog shown when an error occurs while the user is trying to re-enroll their fingerprint. [CHAR LIMIT=NONE] -->
<string name="fingerprint_reenroll_failure_dialog_content">Couldn\u2019t set up fingerprint unlock. Go to Settings to try again.</string>
<!-- Notification title shown when the system requires the user to re-enroll their face. [CHAR LIMIT=NONE] -->
<string name="face_re_enroll_notification_title">Set up Face Unlock again</string>
<!-- Name shown for system notifications related to the face unlock feature. [CHAR LIMIT=NONE] -->
<string name="face_re_enroll_notification_name">Face Unlock</string>
<!-- Title for a dialog shown when the system requires the user to re-enroll their face. [CHAR LIMIT=NONE] -->
<string name="face_re_enroll_dialog_title">Set up Face Unlock</string>
<!-- Content for a dialog shown when the system requires the user to re-enroll their face. [CHAR LIMIT=NONE] -->
<string name="face_re_enroll_dialog_content">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.</string>
<!-- Content for a dialog shown when an error occurs while the user is trying to re-enroll their face. [CHAR LIMIT=NONE] -->
<string name="face_reenroll_failure_dialog_content">Couldn\u2019t set up face unlock. Go to Settings to try again.</string>
<!-- Message shown when the system-provided fingerprint dialog is shown, asking for authentication -->
<string name="fingerprint_dialog_touch_sensor">Touch the fingerprint sensor</string>
<!-- Message shown to inform the user a face cannot be recognized and fingerprint should instead be used.[CHAR LIMIT=50] -->

View File

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

View File

@@ -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);
}
}
});
}
}

View File

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

View File

@@ -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

View File

@@ -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<DialogInterface.OnClickListener> mOnClickListenerArgumentCaptor =
ArgumentCaptor.forClass(DialogInterface.OnClickListener.class);
private final ArgumentCaptor<Intent> 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<FingerprintManager.RemovalCallback> 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<FingerprintManager.RemovalCallback> 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<FaceManager.RemovalCallback> 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<FaceManager.RemovalCallback> 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());
}
}

View File

@@ -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<Notification> 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<KeyguardUpdateMonitorCallback> updateMonitorCallbackArgumentCaptor =
ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
ArgumentCaptor<KeyguardStateController.Callback> 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);
}
}