Merge "Face and fingerprint re-enrollment notification" into udc-dev
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -10202,9 +10202,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
|
||||
*/
|
||||
|
||||
@@ -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] -->
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user