/* * 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 android.app.PendingIntent.FLAG_UPDATE_CURRENT; 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.annotation.NonNull; import android.annotation.Nullable; 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.BiometricSourceType; import android.hardware.biometrics.BiometricStateListener; import android.hardware.face.FaceManager; import android.hardware.fingerprint.FingerprintManager; 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.dagger.SysUISingleton; import com.android.systemui.res.R; import com.android.systemui.statusbar.policy.KeyguardStateController; import java.util.Optional; 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 final FingerprintReEnrollNotification mFingerprintReEnrollNotification; private final FingerprintManager mFingerprintManager; private final FaceManager mFaceManager; private NotificationChannel mNotificationChannel; private boolean mFaceNotificationQueued; private boolean mFingerprintNotificationQueued; private boolean mFingerprintReenrollRequired; private boolean mIsFingerprintReenrollForced; 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); } } @Override public void onBiometricHelp(int msgId, String helpString, BiometricSourceType biometricSourceType) { if (biometricSourceType == BiometricSourceType.FINGERPRINT && mFingerprintReEnrollNotification.isFingerprintReEnrollRequested( msgId)) { mFingerprintReenrollRequired = true; mIsFingerprintReenrollForced = mFingerprintReEnrollNotification.isFingerprintReEnrollForced(msgId); } } }; private final BiometricStateListener mFaceStateListener = new BiometricStateListener() { @Override public void onEnrollmentsChanged(int userId, int sensorId, boolean hasEnrollments) { mNotificationManager.cancelAsUser(TAG, FACE_NOTIFICATION_ID, UserHandle.CURRENT); } }; private final BiometricStateListener mFingerprintStateListener = new BiometricStateListener() { @Override public void onEnrollmentsChanged(int userId, int sensorId, boolean hasEnrollments) { mNotificationManager.cancelAsUser(TAG, FINGERPRINT_NOTIFICATION_ID, UserHandle.CURRENT); } }; @Inject public BiometricNotificationService(@NonNull Context context, @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor, @NonNull KeyguardStateController keyguardStateController, @NonNull Handler handler, @NonNull NotificationManager notificationManager, @NonNull BiometricNotificationBroadcastReceiver biometricNotificationBroadcastReceiver, @NonNull Optional<FingerprintReEnrollNotification> fingerprintReEnrollNotification, @Nullable FingerprintManager fingerprintManager, @Nullable FaceManager faceManager) { mContext = context; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mKeyguardStateController = keyguardStateController; mHandler = handler; mNotificationManager = notificationManager; mBroadcastReceiver = biometricNotificationBroadcastReceiver; mFingerprintReEnrollNotification = fingerprintReEnrollNotification.orElse( new FingerprintReEnrollNotificationImpl()); mFingerprintManager = fingerprintManager; mFaceManager = faceManager; } @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); if (mFingerprintManager != null) { mFingerprintManager.registerBiometricStateListener(mFingerprintStateListener); } if (mFaceManager != null) { mFaceManager.registerBiometricStateListener(mFaceStateListener); } Settings.Secure.putIntForUser(mContext.getContentResolver(), Settings.Secure.FACE_UNLOCK_RE_ENROLL, REENROLL_NOT_REQUIRED, UserHandle.USER_CURRENT); } private void queueFaceReenrollNotification() { Log.d(TAG, "Face re-enroll notification queued."); 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, false), SHOW_NOTIFICATION_DELAY_MS); } private void queueFingerprintReenrollNotification() { Log.d(TAG, "Fingerprint re-enroll notification queued."); 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, mIsFingerprintReenrollForced), SHOW_NOTIFICATION_DELAY_MS); } private void showNotification(String action, CharSequence title, CharSequence content, CharSequence name, int notificationId, boolean isReenrollForced) { 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); onClickIntent.putExtra(BiometricNotificationBroadcastReceiver.EXTRA_IS_REENROLL_FORCED, isReenrollForced); final PendingIntent onClickPendingIntent = PendingIntent.getBroadcastAsUser(mContext, 0 /* requestCode */, onClickIntent, FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT, 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; } }