Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5b515d8b authored by Shawn Lin's avatar Shawn Lin Committed by Bill Lin
Browse files

[DO NOT MERGE] Support biometric re-enrollment for dangling

Add a re-enroll notification with "set up" and "not now" action buttons.
"set up" : bring users to enroll process.
"not now" : do nothing and cancel notification.

Bug: 331804186
Test: atest FaceInternalEnumerateClientTest
            FingerprintInternalEnumerateClientTest
	    BiometricDanglingReceiverTest
Flag: NONE
Change-Id: Ic2401ae588ae5e2d404dd67d7de67491548fdfa2
Merged-In: Ic2401ae588ae5e2d404dd67d7de67491548fdfa2
parent 8346887e
Loading
Loading
Loading
Loading
+19 −0
Original line number Diff line number Diff line
@@ -6438,4 +6438,23 @@ ul.</string>
    <string name="satellite_notification_open_message">Open Messages</string>
    <!-- Invoke Satellite setting activity of Settings -->
    <string name="satellite_notification_how_it_works">How it works</string>

    <!-- Fingerprint dangling notification title -->
    <string name="fingerprint_dangling_notification_title">Set up Fingerprint Unlock again</string>
    <!-- Fingerprint dangling notification content for only 1 fingerprint deleted -->
    <string name="fingerprint_dangling_notification_msg_1"><xliff:g id="fingerprint">%s</xliff:g> wasn\'t working well and was deleted to improve performance</string>
    <!-- Fingerprint dangling notification content for more than 1 fingerprints deleted -->
    <string name="fingerprint_dangling_notification_msg_2"><xliff:g id="fingerprint">%1$s</xliff:g> and <xliff:g id="fingerprint">%2$s</xliff:g> weren\'t working well and were deleted to improve performance</string>
    <!-- Fingerprint dangling notification content for only 1 fingerprint deleted and no fingerprint left-->
    <string name="fingerprint_dangling_notification_msg_all_deleted_1"><xliff:g id="fingerprint">%s</xliff:g> wasn\'t working well and was deleted. Set it up again to unlock your phone with fingerprint.</string>
    <!-- Fingerprint dangling notification content for more than 1 fingerprints deleted and no fingerprint left  -->
    <string name="fingerprint_dangling_notification_msg_all_deleted_2"><xliff:g id="fingerprint">%1$s</xliff:g> and <xliff:g id="fingerprint">%2$s</xliff:g> weren\'t working well and were deleted. Set them up again to unlock your phone with your fingerprint.</string>
    <!-- Face dangling notification title -->
    <string name="face_dangling_notification_title">Set up Face Unlock again</string>
    <!-- Face dangling notification content -->
    <string name="face_dangling_notification_msg">Your face model wasn\'t working well and was deleted. Set it up again to unlock your phone with face.</string>
    <!-- Biometric dangling notification "set up" action button -->
    <string name="biometric_dangling_notification_action_set_up">Set up</string>
    <!-- Biometric dangling notification "Not now" action button -->
    <string name="biometric_dangling_notification_action_not_now">Not now</string>
</resources>
+11 −0
Original line number Diff line number Diff line
@@ -5379,4 +5379,15 @@
  <java-symbol type="string" name="config_defaultContextualSearchKey" />
  <java-symbol type="string" name="config_defaultContextualSearchEnabled" />
  <java-symbol type="string" name="config_defaultContextualSearchLegacyEnabled" />

  <!-- Biometric dangling notification strings -->
  <java-symbol type="string" name="fingerprint_dangling_notification_title" />
  <java-symbol type="string" name="fingerprint_dangling_notification_msg_1" />
  <java-symbol type="string" name="fingerprint_dangling_notification_msg_2" />
  <java-symbol type="string" name="fingerprint_dangling_notification_msg_all_deleted_1" />
  <java-symbol type="string" name="fingerprint_dangling_notification_msg_all_deleted_2" />
  <java-symbol type="string" name="face_dangling_notification_title" />
  <java-symbol type="string" name="face_dangling_notification_msg" />
  <java-symbol type="string" name="biometric_dangling_notification_action_set_up" />
  <java-symbol type="string" name="biometric_dangling_notification_action_not_now" />
</resources>
+93 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.server.biometrics;

import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;

import android.annotation.NonNull;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.biometrics.BiometricsProtoEnums;
import android.provider.Settings;
import android.util.Slog;

import com.android.server.biometrics.sensors.BiometricNotificationUtils;

/**
 * Receives broadcast to biometrics dangling notification.
 */
public class BiometricDanglingReceiver extends BroadcastReceiver {
    private static final String TAG = "BiometricDanglingReceiver";

    public static final String ACTION_FINGERPRINT_RE_ENROLL_LAUNCH =
            "action_fingerprint_re_enroll_launch";
    public static final String ACTION_FINGERPRINT_RE_ENROLL_DISMISS =
            "action_fingerprint_re_enroll_dismiss";

    public static final String ACTION_FACE_RE_ENROLL_LAUNCH =
            "action_face_re_enroll_launch";
    public static final String ACTION_FACE_RE_ENROLL_DISMISS =
            "action_face_re_enroll_dismiss";

    public static final String FACE_SETTINGS_ACTION = "android.settings.FACE_SETTINGS";

    private static final String SETTINGS_PACKAGE = "com.android.settings";

    /**
     * Constructor for BiometricDanglingReceiver.
     *
     * @param context context
     * @param modality the value from BiometricsProtoEnums.MODALITY_*
     */
    public BiometricDanglingReceiver(@NonNull Context context, int modality) {
        final IntentFilter intentFilter = new IntentFilter();
        if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) {
            intentFilter.addAction(ACTION_FINGERPRINT_RE_ENROLL_LAUNCH);
            intentFilter.addAction(ACTION_FINGERPRINT_RE_ENROLL_DISMISS);
        } else if (modality == BiometricsProtoEnums.MODALITY_FACE) {
            intentFilter.addAction(ACTION_FACE_RE_ENROLL_LAUNCH);
            intentFilter.addAction(ACTION_FACE_RE_ENROLL_DISMISS);
        }
        context.registerReceiver(this, intentFilter);
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        Slog.d(TAG, "Received: " + intent.getAction());
        if (ACTION_FINGERPRINT_RE_ENROLL_LAUNCH.equals(intent.getAction())) {
            launchBiometricEnrollActivity(context, Settings.ACTION_FINGERPRINT_ENROLL);
            BiometricNotificationUtils.cancelFingerprintReEnrollNotification(context);
        } else if (ACTION_FINGERPRINT_RE_ENROLL_DISMISS.equals(intent.getAction())) {
            BiometricNotificationUtils.cancelFingerprintReEnrollNotification(context);
        } else if (ACTION_FACE_RE_ENROLL_LAUNCH.equals(intent.getAction())) {
            launchBiometricEnrollActivity(context, FACE_SETTINGS_ACTION);
            BiometricNotificationUtils.cancelFaceReEnrollNotification(context);
        } else if (ACTION_FACE_RE_ENROLL_DISMISS.equals(intent.getAction())) {
            BiometricNotificationUtils.cancelFaceReEnrollNotification(context);
        }
        context.unregisterReceiver(this);
    }

    private void launchBiometricEnrollActivity(Context context, String action) {
        context.sendBroadcast(new Intent(ACTION_CLOSE_SYSTEM_DIALOGS));
        final Intent intent = new Intent(action);
        intent.setPackage(SETTINGS_PACKAGE);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }
}
+141 −3
Original line number Diff line number Diff line
@@ -24,13 +24,18 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.face.FaceEnrollOptions;
import android.hardware.fingerprint.FingerprintEnrollOptions;
import android.os.SystemClock;
import android.os.UserHandle;
import android.text.BidiFormatter;
import android.util.Slog;

import com.android.internal.R;
import com.android.server.biometrics.BiometricDanglingReceiver;

import java.util.List;

/**
 * Biometric notification helper class.
@@ -39,6 +44,7 @@ public class BiometricNotificationUtils {

    private static final String TAG = "BiometricNotificationUtils";
    private static final String FACE_RE_ENROLL_NOTIFICATION_TAG = "FaceReEnroll";
    private static final String FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG = "FingerprintReEnroll";
    private static final String BAD_CALIBRATION_NOTIFICATION_TAG = "FingerprintBadCalibration";
    private static final String KEY_RE_ENROLL_FACE = "re_enroll_face_unlock";
    private static final String FACE_SETTINGS_ACTION = "android.settings.FACE_SETTINGS";
@@ -50,6 +56,8 @@ public class BiometricNotificationUtils {
    private static final String FACE_ENROLL_CHANNEL = "FaceEnrollNotificationChannel";
    private static final String FACE_RE_ENROLL_CHANNEL = "FaceReEnrollNotificationChannel";
    private static final String FINGERPRINT_ENROLL_CHANNEL = "FingerprintEnrollNotificationChannel";
    private static final String FINGERPRINT_RE_ENROLL_CHANNEL =
            "FingerprintReEnrollNotificationChannel";
    private static final String FINGERPRINT_BAD_CALIBRATION_CHANNEL =
            "FingerprintBadCalibrationNotificationChannel";
    private static final long NOTIFICATION_INTERVAL_MS = 24 * 60 * 60 * 1000;
@@ -177,10 +185,124 @@ public class BiometricNotificationUtils {
                BAD_CALIBRATION_NOTIFICATION_TAG, Notification.VISIBILITY_SECRET, false);
    }

    /**
     * Shows a biometric re-enroll notification.
     */
    public static void showBiometricReEnrollNotification(@NonNull Context context,
            @NonNull List<String> identifiers, boolean allIdentifiersDeleted, int modality) {
        final boolean isFingerprint = modality == BiometricsProtoEnums.MODALITY_FINGERPRINT;
        final String reEnrollName = isFingerprint ? FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG
                : FACE_RE_ENROLL_NOTIFICATION_TAG;
        if (identifiers.isEmpty()) {
            Slog.v(TAG, "Skipping " + reEnrollName + " notification : empty list");
            return;
        }
        Slog.d(TAG, "Showing " + reEnrollName + " notification :[" + identifiers.size()
                + " identifier(s) deleted, allIdentifiersDeleted=" + allIdentifiersDeleted + "]");

        final String name =
                context.getString(R.string.device_unlock_notification_name);
        final String title = context.getString(isFingerprint
                ? R.string.fingerprint_dangling_notification_title
                : R.string.face_dangling_notification_title);
        final String content = isFingerprint
                ? getFingerprintDanglingContentString(context, identifiers, allIdentifiersDeleted)
                : context.getString(R.string.face_dangling_notification_msg);

        // Create "Set up" notification action button.
        final Intent setupIntent = new Intent(
                isFingerprint ? BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_LAUNCH
                : BiometricDanglingReceiver.ACTION_FACE_RE_ENROLL_LAUNCH);
        final PendingIntent setupPendingIntent = PendingIntent.getBroadcastAsUser(context, 0,
                setupIntent, PendingIntent.FLAG_IMMUTABLE, UserHandle.CURRENT);
        final String setupText =
                context.getString(R.string.biometric_dangling_notification_action_set_up);
        final Notification.Action setupAction = new Notification.Action.Builder(
                null, setupText, setupPendingIntent).build();

        // Create "Not now" notification action button.
        final Intent notNowIntent = new Intent(
                isFingerprint ? BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_DISMISS
                : BiometricDanglingReceiver.ACTION_FACE_RE_ENROLL_DISMISS);
        final PendingIntent notNowPendingIntent = PendingIntent.getBroadcastAsUser(context, 0,
                notNowIntent, PendingIntent.FLAG_IMMUTABLE, UserHandle.CURRENT);
        final String notNowText = context.getString(
                R.string.biometric_dangling_notification_action_not_now);
        final Notification.Action notNowAction = new Notification.Action.Builder(
                null, notNowText, notNowPendingIntent).build();

        final String channel = isFingerprint ? FINGERPRINT_RE_ENROLL_CHANNEL
                : FACE_RE_ENROLL_CHANNEL;
        final String tag = isFingerprint ? FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG
                : FACE_RE_ENROLL_NOTIFICATION_TAG;

        showNotificationHelper(context, name, title, content, setupPendingIntent, setupAction,
                notNowAction, Notification.CATEGORY_SYSTEM, channel, tag,
                Notification.VISIBILITY_SECRET, false);
    }

    private static String getFingerprintDanglingContentString(Context context,
            @NonNull List<String> fingerprints, boolean allFingerprintDeleted) {
        if (fingerprints.isEmpty()) {
            return null;
        }

        final int resId;
        final int size = fingerprints.size();
        final StringBuilder first = new StringBuilder();
        final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
        if (size > 1) {
            // If there are more than 1 fingerprint deleted, the "second" will be the last
            // fingerprint and set the others to "first".
            // For example, if we have 3 fingerprints deleted(fp1, fp2 and fp3):
            //   first  = "fp1, fp2"
            //   second = "fp3"
            final String separator = ", ";
            String second = null;
            for (int i = 0; i < size; i++) {
                if (i == size - 1) {
                    second = bidiFormatter.unicodeWrap("\"" + fingerprints.get(i) + "\"");
                } else {
                    first.append(bidiFormatter.unicodeWrap("\""));
                    first.append(bidiFormatter.unicodeWrap(fingerprints.get(i)));
                    first.append(bidiFormatter.unicodeWrap("\""));
                    if (i < size - 2) {
                        first.append(bidiFormatter.unicodeWrap(separator));
                    }
                }
            }
            if (allFingerprintDeleted) {
                resId = R.string.fingerprint_dangling_notification_msg_all_deleted_2;
            } else {
                resId = R.string.fingerprint_dangling_notification_msg_2;
            }

            return String.format(context.getString(resId), first, second);
        } else {
            if (allFingerprintDeleted) {
                resId = R.string.fingerprint_dangling_notification_msg_all_deleted_1;
            } else {
                resId = R.string.fingerprint_dangling_notification_msg_1;
            }
            first.append(bidiFormatter.unicodeWrap("\""));
            first.append(bidiFormatter.unicodeWrap(fingerprints.get(0)));
            first.append(bidiFormatter.unicodeWrap("\""));
            return String.format(context.getString(resId), first);
        }
    }

    private static void showNotificationHelper(Context context, String name, String title,
                String content, PendingIntent pendingIntent, String category,
                String channelName, String notificationTag, int visibility,
                boolean listenToDismissEvent) {
            String content, PendingIntent pendingIntent, String category, String channelName,
            String notificationTag, int visibility, boolean listenToDismissEvent) {
        showNotificationHelper(context, name, title, content, pendingIntent,
                null /* positiveAction */, null /* negativeAction */, category, channelName,
                notificationTag, visibility, listenToDismissEvent);
    }

    private static void showNotificationHelper(Context context, String name, String title,
            String content, PendingIntent pendingIntent, Notification.Action positiveAction,
            Notification.Action negativeAction, String category, String channelName,
            String notificationTag, int visibility, boolean listenToDismissEvent) {
        Slog.v(TAG," listenToDismissEvent = " + listenToDismissEvent);
        final PendingIntent dismissIntent = PendingIntent.getActivityAsUser(context,
                0 /* requestCode */, DISMISS_FRR_INTENT, PendingIntent.FLAG_IMMUTABLE /* flags */,
@@ -202,6 +324,12 @@ public class BiometricNotificationUtils {
                .setContentIntent(pendingIntent)
                .setVisibility(visibility);

        if (positiveAction != null) {
            builder.addAction(positiveAction);
        }
        if (negativeAction != null) {
            builder.addAction(negativeAction);
        }
        if (listenToDismissEvent) {
            builder.setDeleteIntent(dismissIntent);
        }
@@ -253,4 +381,14 @@ public class BiometricNotificationUtils {
                UserHandle.CURRENT);
    }

    /**
     * Cancels a fingerprint enrollment notification
     */
    public static void cancelFingerprintReEnrollNotification(@NonNull Context context) {
        final NotificationManager notificationManager =
                context.getSystemService(NotificationManager.class);
        notificationManager.cancelAsUser(FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG, NOTIFICATION_ID,
                UserHandle.CURRENT);
    }

}
+26 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.hardware.biometrics.BiometricAuthenticator;
import android.os.IBinder;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.BiometricsProto;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
@@ -44,6 +45,7 @@ public abstract class InternalEnumerateClient<T> extends HalClientMonitor<T>
    private List<? extends BiometricAuthenticator.Identifier> mEnrolledList;
    // List of templates to remove from the HAL
    private List<BiometricAuthenticator.Identifier> mUnknownHALTemplates = new ArrayList<>();
    private final int mInitialEnrolledSize;

    protected InternalEnumerateClient(@NonNull Context context, @NonNull Supplier<T> lazyDaemon,
            @NonNull IBinder token, int userId, @NonNull String owner,
@@ -55,6 +57,7 @@ public abstract class InternalEnumerateClient<T> extends HalClientMonitor<T>
        super(context, lazyDaemon, token, null /* ClientMonitorCallbackConverter */, userId, owner,
                0 /* cookie */, sensorId, logger, biometricContext);
        mEnrolledList = enrolledList;
        mInitialEnrolledSize = mEnrolledList.size();
        mUtils = utils;
    }

@@ -111,8 +114,10 @@ public abstract class InternalEnumerateClient<T> extends HalClientMonitor<T>

        // At this point, mEnrolledList only contains templates known to the framework and
        // not the HAL.
        final List<String> names = new ArrayList<>();
        for (int i = 0; i < mEnrolledList.size(); i++) {
            BiometricAuthenticator.Identifier identifier = mEnrolledList.get(i);
            names.add(identifier.getName().toString());
            Slog.e(TAG, "doTemplateCleanup(): Removing dangling template from framework: "
                    + identifier.getBiometricId() + " " + identifier.getName());
            mUtils.removeBiometricForUser(getContext(),
@@ -120,6 +125,11 @@ public abstract class InternalEnumerateClient<T> extends HalClientMonitor<T>

            getLogger().logUnknownEnrollmentInFramework();
        }

        // Send dangling notification.
        if (!names.isEmpty()) {
            sendDanglingNotification(names);
        }
        mEnrolledList.clear();
    }

@@ -127,8 +137,24 @@ public abstract class InternalEnumerateClient<T> extends HalClientMonitor<T>
        return mUnknownHALTemplates;
    }

    /**
     * Send the dangling notification.
     */
    @VisibleForTesting
    public void sendDanglingNotification(@NonNull List<String> identifierNames) {
        if (!identifierNames.isEmpty()) {
            Slog.e(TAG, "sendDanglingNotification(): initial enrolledSize="
                    + mInitialEnrolledSize + ", after clean up size=" + mEnrolledList.size());
            final boolean allIdentifiersDeleted = mEnrolledList.size() == mInitialEnrolledSize;
            BiometricNotificationUtils.showBiometricReEnrollNotification(
                    getContext(), identifierNames, allIdentifiersDeleted, getModality());
        }
    }

    @Override
    public int getProtoEnum() {
        return BiometricsProto.CM_ENUMERATE;
    }

    protected abstract int getModality();
}
Loading