| /* |
| * Copyright (C) 2020 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.settings.biometrics; |
| |
| import android.app.Activity; |
| import android.app.PendingIntent; |
| import android.app.admin.DevicePolicyManager; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentSender; |
| import android.hardware.biometrics.SensorProperties; |
| import android.hardware.face.FaceManager; |
| import android.hardware.face.FaceSensorPropertiesInternal; |
| import android.os.storage.StorageManager; |
| import android.util.Log; |
| import android.view.Surface; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.fragment.app.FragmentActivity; |
| |
| import com.android.internal.widget.LockPatternUtils; |
| import com.android.internal.widget.VerifyCredentialResponse; |
| import com.android.settings.SetupWizardUtils; |
| import com.android.settings.biometrics.face.FaceEnrollIntroduction; |
| import com.android.settings.biometrics.fingerprint.FingerprintEnrollFindSensor; |
| import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroduction; |
| import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction; |
| import com.android.settings.password.ChooseLockGeneric; |
| import com.android.settings.password.ChooseLockSettingsHelper; |
| import com.android.settings.password.SetupChooseLockGeneric; |
| |
| import com.google.android.setupcompat.util.WizardManagerHelper; |
| |
| /** |
| * Common biometric utilities. |
| */ |
| public class BiometricUtils { |
| private static final String TAG = "BiometricUtils"; |
| |
| /** |
| * Request was sent for starting another enrollment of a previously |
| * enrolled biometric of the same type. |
| */ |
| public static int REQUEST_ADD_ANOTHER = 7; |
| /** |
| * Given the result from confirming or choosing a credential, request Gatekeeper to generate |
| * a HardwareAuthToken with the Gatekeeper Password together with a biometric challenge. |
| * |
| * @param context Caller's context |
| * @param result The onActivityResult intent from ChooseLock* or ConfirmLock* |
| * @param userId User ID that the credential/biometric operation applies to |
| * @param challenge Unique biometric challenge from FingerprintManager/FaceManager |
| * @return |
| */ |
| public static byte[] requestGatekeeperHat(@NonNull Context context, @NonNull Intent result, |
| int userId, long challenge) { |
| if (!containsGatekeeperPasswordHandle(result)) { |
| throw new IllegalStateException("Gatekeeper Password is missing!!"); |
| } |
| final long gatekeeperPasswordHandle = result.getLongExtra( |
| ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 0L); |
| return requestGatekeeperHat(context, gatekeeperPasswordHandle, userId, challenge); |
| } |
| |
| public static byte[] requestGatekeeperHat(@NonNull Context context, long gkPwHandle, int userId, |
| long challenge) { |
| final LockPatternUtils utils = new LockPatternUtils(context); |
| final VerifyCredentialResponse response = utils.verifyGatekeeperPasswordHandle(gkPwHandle, |
| challenge, userId); |
| if (!response.isMatched()) { |
| throw new IllegalStateException("Unable to request Gatekeeper HAT"); |
| } |
| return response.getGatekeeperHAT(); |
| } |
| |
| public static boolean containsGatekeeperPasswordHandle(@Nullable Intent data) { |
| return data != null && data.hasExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE); |
| } |
| |
| public static long getGatekeeperPasswordHandle(@NonNull Intent data) { |
| return data.getLongExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 0L); |
| } |
| |
| /** |
| * Requests {@link com.android.server.locksettings.LockSettingsService} to remove the |
| * gatekeeper password associated with a previous |
| * {@link ChooseLockSettingsHelper.Builder#setRequestGatekeeperPasswordHandle(boolean)} |
| * |
| * @param context Caller's context |
| * @param data The onActivityResult intent from ChooseLock* or ConfirmLock* |
| */ |
| public static void removeGatekeeperPasswordHandle(@NonNull Context context, |
| @Nullable Intent data) { |
| if (data == null) { |
| return; |
| } |
| if (!containsGatekeeperPasswordHandle(data)) { |
| return; |
| } |
| removeGatekeeperPasswordHandle(context, getGatekeeperPasswordHandle(data)); |
| } |
| |
| public static void removeGatekeeperPasswordHandle(@NonNull Context context, long handle) { |
| final LockPatternUtils utils = new LockPatternUtils(context); |
| utils.removeGatekeeperPasswordHandle(handle); |
| Log.d(TAG, "Removed handle"); |
| } |
| |
| /** |
| * @param context caller's context |
| * @param activityIntent The intent that started the caller's activity |
| * @return Intent for starting ChooseLock* |
| */ |
| public static Intent getChooseLockIntent(@NonNull Context context, |
| @NonNull Intent activityIntent) { |
| if (WizardManagerHelper.isAnySetupWizard(activityIntent)) { |
| // Default to PIN lock in setup wizard |
| Intent intent = new Intent(context, SetupChooseLockGeneric.class); |
| if (StorageManager.isFileEncryptedNativeOrEmulated()) { |
| intent.putExtra( |
| LockPatternUtils.PASSWORD_TYPE_KEY, |
| DevicePolicyManager.PASSWORD_QUALITY_NUMERIC); |
| intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment |
| .EXTRA_SHOW_OPTIONS_BUTTON, true); |
| } |
| WizardManagerHelper.copyWizardManagerExtras(activityIntent, intent); |
| return intent; |
| } else { |
| return new Intent(context, ChooseLockGeneric.class); |
| } |
| } |
| |
| /** |
| * @param context caller's context |
| * @param activityIntent The intent that started the caller's activity |
| * @return Intent for starting FingerprintEnrollFindSensor |
| */ |
| public static Intent getFingerprintFindSensorIntent(@NonNull Context context, |
| @NonNull Intent activityIntent) { |
| Intent intent = new Intent(context, FingerprintEnrollFindSensor.class); |
| SetupWizardUtils.copySetupExtras(activityIntent, intent); |
| return intent; |
| } |
| |
| /** |
| * @param context caller's context |
| * @param activityIntent The intent that started the caller's activity |
| * @return Intent for starting FingerprintEnrollIntroduction |
| */ |
| public static Intent getFingerprintIntroIntent(@NonNull Context context, |
| @NonNull Intent activityIntent) { |
| if (WizardManagerHelper.isAnySetupWizard(activityIntent)) { |
| Intent intent = new Intent(context, SetupFingerprintEnrollIntroduction.class); |
| WizardManagerHelper.copyWizardManagerExtras(activityIntent, intent); |
| return intent; |
| } else { |
| return new Intent(context, FingerprintEnrollIntroduction.class); |
| } |
| } |
| |
| /** |
| * @param context caller's context |
| * @param activityIntent The intent that started the caller's activity |
| * @return Intent for starting FaceEnrollIntroduction |
| */ |
| public static Intent getFaceIntroIntent(@NonNull Context context, |
| @NonNull Intent activityIntent) { |
| final Intent intent = new Intent(context, FaceEnrollIntroduction.class); |
| WizardManagerHelper.copyWizardManagerExtras(activityIntent, intent); |
| return intent; |
| } |
| |
| /** |
| * Start an activity that prompts the user to hand the device to their parent or guardian. |
| * @param context caller's context |
| * @param activityIntent The intent that started the caller's activity |
| * @return Intent for starting BiometricHandoffActivity |
| */ |
| public static Intent getHandoffToParentIntent(@NonNull Context context, |
| @NonNull Intent activityIntent) { |
| final Intent intent = new Intent(context, BiometricHandoffActivity.class); |
| WizardManagerHelper.copyWizardManagerExtras(activityIntent, intent); |
| return intent; |
| } |
| |
| /** |
| * @param activity Reference to the calling activity, used to startActivity |
| * @param intent Intent pointing to the enrollment activity |
| * @param requestCode If non-zero, will invoke startActivityForResult instead of startActivity |
| * @param hardwareAuthToken HardwareAuthToken from Gatekeeper |
| * @param userId User to request enrollment for |
| */ |
| public static void launchEnrollForResult(@NonNull FragmentActivity activity, |
| @NonNull Intent intent, int requestCode, |
| @Nullable byte[] hardwareAuthToken, @Nullable Long gkPwHandle, int userId) { |
| if (hardwareAuthToken != null) { |
| intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, |
| hardwareAuthToken); |
| } |
| if (gkPwHandle != null) { |
| intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, (long) gkPwHandle); |
| } |
| |
| if (activity instanceof BiometricEnrollActivity.InternalActivity) { |
| intent.putExtra(Intent.EXTRA_USER_ID, userId); |
| } |
| |
| if (requestCode != 0) { |
| activity.startActivityForResult(intent, requestCode); |
| } else { |
| activity.startActivity(intent); |
| activity.finish(); |
| } |
| } |
| |
| /** |
| * Used for checking if a multi-biometric enrollment flow starts with Face and |
| * ends with Fingerprint. |
| * |
| * @param activity Activity that we want to check |
| * @return True if the activity is going through a multi-biometric enrollment flow, that starts |
| * with Face. |
| */ |
| public static boolean isMultiBiometricFaceEnrollmentFlow(@NonNull Activity activity) { |
| return activity.getIntent().hasExtra(MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FACE); |
| } |
| |
| /** |
| * Used for checking if a multi-biometric enrollment flowstarts with Fingerprint |
| * and ends with Face. |
| * |
| * @param activity Activity that we want to check |
| * @return True if the activity is going through a multi-biometric enrollment flow, that starts |
| * with Fingerprint. |
| */ |
| public static boolean isMultiBiometricFingerprintEnrollmentFlow(@NonNull Activity activity) { |
| return activity.getIntent().hasExtra( |
| MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FINGERPRINT); |
| } |
| |
| /** |
| * Used to check if the activity is a multi biometric flow activity. |
| * |
| * @param activity Activity to check |
| * @return True if the activity is going through a multi-biometric enrollment flow, that starts |
| * with Fingerprint. |
| */ |
| public static boolean isAnyMultiBiometricFlow(@NonNull Activity activity) { |
| return isMultiBiometricFaceEnrollmentFlow(activity) |
| || isMultiBiometricFingerprintEnrollmentFlow(activity); |
| } |
| |
| public static void copyMultiBiometricExtras(@NonNull Intent fromIntent, |
| @NonNull Intent toIntent) { |
| PendingIntent pendingIntent = (PendingIntent) fromIntent.getExtra( |
| MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FACE, null); |
| if (pendingIntent != null) { |
| toIntent.putExtra(MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FACE, |
| pendingIntent); |
| } |
| |
| pendingIntent = (PendingIntent) fromIntent.getExtra( |
| MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FINGERPRINT, null); |
| if (pendingIntent != null) { |
| toIntent.putExtra(MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FINGERPRINT, |
| pendingIntent); |
| } |
| } |
| |
| /** |
| * If the current biometric enrollment (e.g. face/fingerprint) should be followed by another |
| * one (e.g. fingerprint/face) retrieves the PendingIntent pointing to the next enrollment |
| * and starts it. The caller will receive the result in onActivityResult. |
| * @return true if the next enrollment was started |
| */ |
| public static boolean tryStartingNextBiometricEnroll(@NonNull Activity activity, |
| int requestCode, String debugReason) { |
| |
| PendingIntent pendingIntent = (PendingIntent) activity.getIntent() |
| .getExtra(MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FACE); |
| if (pendingIntent == null) { |
| pendingIntent = (PendingIntent) activity.getIntent() |
| .getExtra(MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FINGERPRINT); |
| } |
| |
| if (pendingIntent != null) { |
| try { |
| IntentSender intentSender = pendingIntent.getIntentSender(); |
| activity.startIntentSenderForResult(intentSender, requestCode, |
| null /* fillInIntent */, 0 /* flagMask */, 0 /* flagValues */, |
| 0 /* extraFlags */); |
| return true; |
| } catch (IntentSender.SendIntentException e) { |
| Log.e(TAG, "Pending intent canceled: " + e); |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns {@code true} if the screen is going into a landscape mode and the angle is equal to |
| * 270. |
| * @param context Context that we use to get the display this context is associated with |
| * @return True if the angle of the rotation is equal to 270. |
| */ |
| public static boolean isReverseLandscape(@NonNull Context context) { |
| return context.getDisplay().getRotation() == Surface.ROTATION_270; |
| } |
| |
| /** |
| * @param faceManager |
| * @return True if at least one sensor is set as a convenience. |
| */ |
| public static boolean isConvenience(@NonNull FaceManager faceManager) { |
| for (FaceSensorPropertiesInternal props : faceManager.getSensorPropertiesInternal()) { |
| if (props.sensorStrength == SensorProperties.STRENGTH_CONVENIENCE) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns {@code true} if the screen is going into a landscape mode and the angle is equal to |
| * 90. |
| * @param context Context that we use to get the display this context is associated with |
| * @return True if the angle of the rotation is equal to 90. |
| */ |
| public static boolean isLandscape(@NonNull Context context) { |
| return context.getDisplay().getRotation() == Surface.ROTATION_90; |
| } |
| } |