/*
* Copyright 2018 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 androidx.biometric;
import android.annotation.SuppressLint;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.biometric.BiometricManager.Authenticators;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.security.Signature;
import java.util.concurrent.Executor;
import javax.crypto.Cipher;
import javax.crypto.Mac;
/**
* A class that manages a system-provided biometric prompt. On devices running Android 9.0 (API 28)
* and above, this will show a system-provided authentication prompt, using one of the device's
* supported biometric modalities (fingerprint, iris, face, etc). Prior to Android 9.0, this will
* instead show a custom fingerprint authentication dialog. The prompt will persist across
* configuration changes unless explicitly canceled. For security reasons, the prompt will be
* dismissed when the client application is no longer in the foreground.
*
* <p>To persist authentication across configuration changes, developers should (re)create the
* prompt every time the activity/fragment is created. Instantiating the prompt with a new
* callback early in the fragment/activity lifecycle (e.g. in {@code onCreate()}) will allow the
* ongoing authentication session's callbacks to be received by the new fragment/activity instance.
* Note that {@code cancelAuthentication()} should not be called, and {@code authenticate()} does
* not need to be invoked during activity/fragment creation.
*/
public class BiometricPrompt {
private static final String TAG = "BiometricPromptCompat";
/**
* There is no error, and the user can successfully authenticate.
*/
static final int BIOMETRIC_SUCCESS = 0;
/**
* The hardware is unavailable. Try again later.
*/
public static final int ERROR_HW_UNAVAILABLE = 1;
/**
* The sensor was unable to process the current image.
*/
public static final int ERROR_UNABLE_TO_PROCESS = 2;
/**
* The current operation has been running too long and has timed out.
*
* <p>This is intended to prevent programs from waiting for the biometric sensor indefinitely.
* The timeout is platform and sensor-specific, but is generally on the order of ~30 seconds.
*/
public static final int ERROR_TIMEOUT = 3;
/**
* The operation can't be completed because there is not enough device storage remaining.
*/
public static final int ERROR_NO_SPACE = 4;
/**
* The operation was canceled because the biometric sensor is unavailable. This may happen when
* the user is switched, the device is locked, or another pending operation prevents it.
*/
public static final int ERROR_CANCELED = 5;
/**
* The operation was canceled because the API is locked out due to too many attempts. This
* occurs after 5 failed attempts, and lasts for 30 seconds.
*/
public static final int ERROR_LOCKOUT = 7;
/**
* The operation failed due to a vendor-specific error.
*
* <p>This error code may be used by hardware vendors to extend this list to cover errors that
* don't fall under one of the other predefined categories. Vendors are responsible for
* providing the strings for these errors.
*
* <p>These messages are typically reserved for internal operations such as enrollment but may
* be used to express any error that is not otherwise covered. In this case, applications are
* expected to show the error message, but they are advised not to rely on the message ID, since
* this may vary by vendor and device.
*/
public static final int ERROR_VENDOR = 8;
/**
* The operation was canceled because {@link #ERROR_LOCKOUT} occurred too many times. Biometric
* authentication is disabled until the user unlocks with their device credential (i.e. PIN,
* pattern, or password).
*/
public static final int ERROR_LOCKOUT_PERMANENT = 9;
/**
* The user canceled the operation.
*
* <p>Upon receiving this, applications should use alternate authentication, such as a password.
* The application should also provide the user a way of returning to biometric authentication,
* such as a button.
*/
public static final int ERROR_USER_CANCELED = 10;
/**
* The user does not have any biometrics enrolled.
*/
public static final int ERROR_NO_BIOMETRICS = 11;
/**
* The device does not have the required authentication hardware.
*/
public static final int ERROR_HW_NOT_PRESENT = 12;
/**
* The user pressed the negative button.
*/
public static final int ERROR_NEGATIVE_BUTTON = 13;
/**
* The device does not have pin, pattern, or password set up.
*/
public static final int ERROR_NO_DEVICE_CREDENTIAL = 14;
/**
* A security vulnerability has been discovered with one or more hardware sensors. The
* affected sensor(s) are unavailable until a security update has addressed the issue.
*/
public static final int ERROR_SECURITY_UPDATE_REQUIRED = 15;
/**
* An error code that may be returned during authentication.
*/
@IntDef({
ERROR_HW_UNAVAILABLE,
ERROR_UNABLE_TO_PROCESS,
ERROR_TIMEOUT,
ERROR_NO_SPACE,
ERROR_CANCELED,
ERROR_LOCKOUT,
ERROR_VENDOR,
ERROR_LOCKOUT_PERMANENT,
ERROR_USER_CANCELED,
ERROR_NO_BIOMETRICS,
ERROR_HW_NOT_PRESENT,
ERROR_NEGATIVE_BUTTON,
ERROR_NO_DEVICE_CREDENTIAL
})
@Retention(RetentionPolicy.SOURCE)
@interface AuthenticationError {}
/**
* Authentication type reported by {@link AuthenticationResult} when the user authenticated via
* an unknown method.
*
* <p>This value may be returned on older Android versions due to partial incompatibility
* with a newer API. It does NOT necessarily imply that the user authenticated with a method
* other than those represented by {@link #AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL} and
* {@link #AUTHENTICATION_RESULT_TYPE_BIOMETRIC}.
*/
public static final int AUTHENTICATION_RESULT_TYPE_UNKNOWN = -1;
/**
* Authentication type reported by {@link AuthenticationResult} when the user authenticated by
* entering their device PIN, pattern, or password.
*/
public static final int AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL = 1;
/**
* Authentication type reported by {@link AuthenticationResult} when the user authenticated by
* presenting some form of biometric (e.g. fingerprint or face).
*/
public static final int AUTHENTICATION_RESULT_TYPE_BIOMETRIC = 2;
/**
* The authentication type that was used, as reported by {@link AuthenticationResult}.
*/
@IntDef({
AUTHENTICATION_RESULT_TYPE_UNKNOWN,
AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL,
AUTHENTICATION_RESULT_TYPE_BIOMETRIC
})
@Retention(RetentionPolicy.SOURCE)
@interface AuthenticationResultType {}
/**
* Tag used to identify the {@link BiometricFragment} attached to the client activity/fragment.
*/
private static final String BIOMETRIC_FRAGMENT_TAG = "androidx.biometric.BiometricFragment";
/**
* A wrapper class for the crypto objects supported by {@link BiometricPrompt}.
*/
public static class CryptoObject {
@Nullable private final Signature mSignature;
@Nullable private final Cipher mCipher;
@Nullable private final Mac mMac;
@Nullable private final android.security.identity.IdentityCredential mIdentityCredential;
/**
* Creates a crypto object that wraps the given signature object.
*
* @param signature The signature to be associated with this crypto object.
*/
public CryptoObject(@NonNull Signature signature) {
mSignature = signature;
mCipher = null;
mMac = null;
mIdentityCredential = null;
}
/**
* Creates a crypto object that wraps the given cipher object.
*
* @param cipher The cipher to be associated with this crypto object.
*/
public CryptoObject(@NonNull Cipher cipher) {
mSignature = null;
mCipher = cipher;
mMac = null;
mIdentityCredential = null;
}
/**
* Creates a crypto object that wraps the given MAC object.
*
* @param mac The MAC to be associated with this crypto object.
*/
public CryptoObject(@NonNull Mac mac) {
mSignature = null;
mCipher = null;
mMac = mac;
mIdentityCredential = null;
}
/**
* Creates a crypto object that wraps the given identity credential object.
*
* @param identityCredential The identity credential to be associated with this crypto
* object.
*/
@RequiresApi(Build.VERSION_CODES.R)
public CryptoObject(
@NonNull android.security.identity.IdentityCredential identityCredential) {
mSignature = null;
mCipher = null;
mMac = null;
mIdentityCredential = identityCredential;
}
/**
* Gets the signature object associated with this crypto object.
*
* @return The signature, or {@code null} if none is associated with this object.
*/
@Nullable
public Signature getSignature() {
return mSignature;
}
/**
* Gets the cipher object associated with this crypto object.
*
* @return The cipher, or {@code null} if none is associated with this object.
*/
@Nullable
public Cipher getCipher() {
return mCipher;
}
/**
* Gets the MAC object associated with this crypto object.
*
* @return The MAC, or {@code null} if none is associated with this object.
*/
@Nullable
public Mac getMac() {
return mMac;
}
/**
* Gets the identity credential object associated with this crypto object.
*
* @return The identity credential, or {@code null} if none is associated with this object.
*/
@RequiresApi(Build.VERSION_CODES.R)
@Nullable
public android.security.identity.IdentityCredential getIdentityCredential() {
return mIdentityCredential;
}
}
/**
* A container for data passed to {@link AuthenticationCallback#onAuthenticationSucceeded(
* AuthenticationResult)} when the user has successfully authenticated.
*/
public static class AuthenticationResult {
private final CryptoObject mCryptoObject;
@AuthenticationResultType private final int mAuthenticationType;
AuthenticationResult(
CryptoObject crypto, @AuthenticationResultType int authenticationType) {
mCryptoObject = crypto;
mAuthenticationType = authenticationType;
}
/**
* Gets the {@link CryptoObject} associated with this transaction.
*
* @return The {@link CryptoObject} provided to {@code authenticate()}.
*/
@Nullable
public CryptoObject getCryptoObject() {
return mCryptoObject;
}
/**
* Gets the type of authentication (e.g. device credential or biometric) that was
* requested from and successfully provided by the user.
*
* @return An integer representing the type of authentication that was used.
*
* @see #AUTHENTICATION_RESULT_TYPE_UNKNOWN
* @see #AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL
* @see #AUTHENTICATION_RESULT_TYPE_BIOMETRIC
*/
@AuthenticationResultType
public int getAuthenticationType() {
return mAuthenticationType;
}
}
/**
* A collection of methods that may be invoked by {@link BiometricPrompt} during authentication.
*/
public abstract static class AuthenticationCallback {
/**
* Called when an unrecoverable error has been encountered and authentication has stopped.
*
* <p>After this method is called, no further events will be sent for the current
* authentication session.
*
* @param errorCode An integer ID associated with the error.
* @param errString A human-readable string that describes the error.
*/
public void onAuthenticationError(
@AuthenticationError int errorCode, @NonNull CharSequence errString) {}
/**
* Called when a biometric (e.g. fingerprint, face, etc.) is recognized, indicating that the
* user has successfully authenticated.
*
* <p>After this method is called, no further events will be sent for the current
* authentication session.
*
* @param result An object containing authentication-related data.
*/
public void onAuthenticationSucceeded(@NonNull AuthenticationResult result) {}
/**
* Called when a biometric (e.g. fingerprint, face, etc.) is presented but not recognized as
* belonging to the user.
*/
public void onAuthenticationFailed() {}
}
/**
* A set of configurable options for how the {@link BiometricPrompt} should appear and behave.
*/
public static class PromptInfo {
/**
* A builder used to set individual options for the {@link PromptInfo} class.
*/
public static class Builder {
// Mutable options to be set on the builder.
@Nullable private CharSequence mTitle = null;
@Nullable private CharSequence mSubtitle = null;
@Nullable private CharSequence mDescription = null;
@Nullable private CharSequence mNegativeButtonText = null;
private boolean mIsConfirmationRequired = true;
private boolean mIsDeviceCredentialAllowed = false;
@BiometricManager.AuthenticatorTypes private int mAllowedAuthenticators = 0;
/**
* Required: Sets the title for the prompt.
*
* @param title The title to be displayed on the prompt.
* @return This builder.
*/
@NonNull
public Builder setTitle(@NonNull CharSequence title) {
mTitle = title;
return this;
}
/**
* Optional: Sets the subtitle for the prompt.
*
* @param subtitle The subtitle to be displayed on the prompt.
* @return This builder.
*/
@NonNull
public Builder setSubtitle(@Nullable CharSequence subtitle) {
mSubtitle = subtitle;
return this;
}
/**
* Optional: Sets the description for the prompt.
*
* @param description The description to be displayed on the prompt.
* @return This builder.
*/
@NonNull
public Builder setDescription(@Nullable CharSequence description) {
mDescription = description;
return this;
}
/**
* Required: Sets the text for the negative button on the prompt.
*
* <p>Note that this option is incompatible with device credential authentication and
* must NOT be set if the latter is enabled via {@link #setAllowedAuthenticators(int)}
* or {@link #setDeviceCredentialAllowed(boolean)}.
*
* @param negativeButtonText The label to be used for the negative button on the prompt.
* @return This builder.
*/
@SuppressWarnings("deprecation")
@NonNull
public Builder setNegativeButtonText(@NonNull CharSequence negativeButtonText) {
mNegativeButtonText = negativeButtonText;
return this;
}
/**
* Optional: Sets a system hint for whether to require explicit user confirmation after
* a passive biometric (e.g. iris or face) has been recognized but before
* {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)} is
* called. Defaults to {@code true}.
*
* <p>Disabling this option is generally only appropriate for frequent, low-value
* transactions, such as re-authenticating for a previously authorized application.
*
* <p>Also note that, as it is merely a hint, this option may be ignored by the system.
* For example, the system may choose to instead always require confirmation if the user
* has disabled passive authentication for their device in Settings. Additionally, this
* option will be ignored on devices running OS versions prior to Android 10 (API 29).
*
* @param confirmationRequired Whether this option should be enabled.
* @return This builder.
*/
@NonNull
public Builder setConfirmationRequired(boolean confirmationRequired) {
mIsConfirmationRequired = confirmationRequired;
return this;
}
/**
* Optional: Sets whether the user should be given the option to authenticate with
* their device PIN, pattern, or password instead of a biometric. Defaults to
* {@code false}.
*
* <p>Note that this option is incompatible with
* {@link PromptInfo.Builder#setNegativeButtonText(CharSequence)} and must NOT be
* enabled if the latter is set.
*
* <p>Before enabling this option, developers should check whether the device is secure
* by calling {@link android.app.KeyguardManager#isDeviceSecure()}. If the device is not
* secure, authentication will fail with {@link #ERROR_NO_DEVICE_CREDENTIAL}.
*
* <p>On versions prior to Android 10 (API 29), calls to
* {@link #cancelAuthentication()} will not work as expected after the
* user has chosen to authenticate with their device credential. This is because the
* library internally launches a separate activity (by calling
* {@link android.app.KeyguardManager#createConfirmDeviceCredentialIntent(CharSequence,
* CharSequence)}) that does not have a public API for cancellation.
*
* @param deviceCredentialAllowed Whether this option should be enabled.
* @return This builder.
*
* @deprecated Use {@link #setAllowedAuthenticators(int)} instead.
*/
@SuppressWarnings("deprecation")
@Deprecated
@NonNull
public Builder setDeviceCredentialAllowed(boolean deviceCredentialAllowed) {
mIsDeviceCredentialAllowed = deviceCredentialAllowed;
return this;
}
/**
* Optional: Specifies the type(s) of authenticators that may be invoked by
* {@link BiometricPrompt} to authenticate the user. Available authenticator types are
* defined in {@link Authenticators} and can be combined via bitwise OR. Defaults to:
* <ul>
* <li>{@link Authenticators#BIOMETRIC_WEAK} for non-crypto authentication, or</li>
* <li>{@link Authenticators#BIOMETRIC_STRONG} for crypto-based authentication.</li>
* </ul>
*
* <p>Note that not all combinations of authenticator types are supported prior to
* Android 11 (API 30). Specifically, {@code DEVICE_CREDENTIAL} alone is unsupported
* prior to API 30, and {@code BIOMETRIC_STRONG | DEVICE_CREDENTIAL} is unsupported on
* API 28-29. Setting an unsupported value on an affected Android version will result in
* an error when calling {@link #build()}.
*
* <p>This method should be preferred over {@link #setDeviceCredentialAllowed(boolean)}
* and overrides the latter if both are used. Using this method to enable device
* credential authentication (with {@link Authenticators#DEVICE_CREDENTIAL}) will
* replace the negative button on the prompt, making it an error to also call
* {@link #setNegativeButtonText(CharSequence)}.
*
* <p>If this method is used and no authenticator of any of the specified types is
* available at the time {@code authenticate()} is called,
* {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} will be
* invoked with an appropriate error code.
*
* @param allowedAuthenticators A bit field representing all valid authenticator types
* that may be invoked by the prompt.
* @return This builder.
*/
@NonNull
public Builder setAllowedAuthenticators(
@BiometricManager.AuthenticatorTypes int allowedAuthenticators) {
mAllowedAuthenticators = allowedAuthenticators;
return this;
}
/**
* Creates a {@link PromptInfo} object with the specified options.
*
* @return The {@link PromptInfo} object.
*
* @throws IllegalArgumentException If any required option is not set, or if any
* illegal combination of options is present.
*/
@NonNull
public PromptInfo build() {
if (TextUtils.isEmpty(mTitle)) {
throw new IllegalArgumentException("Title must be set and non-empty.");
}
if (!AuthenticatorUtils.isSupportedCombination(mAllowedAuthenticators)) {
throw new IllegalArgumentException("Authenticator combination is unsupported "
+ "on API " + Build.VERSION.SDK_INT + ": "
+ AuthenticatorUtils.convertToString(mAllowedAuthenticators));
}
final boolean isDeviceCredentialAllowed = mAllowedAuthenticators != 0
? AuthenticatorUtils.isDeviceCredentialAllowed(mAllowedAuthenticators)
: mIsDeviceCredentialAllowed;
if (TextUtils.isEmpty(mNegativeButtonText) && !isDeviceCredentialAllowed) {
throw new IllegalArgumentException("Negative text must be set and non-empty.");
}
if (!TextUtils.isEmpty(mNegativeButtonText) && isDeviceCredentialAllowed) {
throw new IllegalArgumentException("Negative text must not be set if device "
+ "credential authentication is allowed.");
}
return new PromptInfo(
mTitle,
mSubtitle,
mDescription,
mNegativeButtonText,
mIsConfirmationRequired,
mIsDeviceCredentialAllowed,
mAllowedAuthenticators);
}
}
// Immutable fields for the prompt info object.
@NonNull private final CharSequence mTitle;
@Nullable private final CharSequence mSubtitle;
@Nullable private final CharSequence mDescription;
@Nullable private final CharSequence mNegativeButtonText;
private final boolean mIsConfirmationRequired;
private final boolean mIsDeviceCredentialAllowed;
@BiometricManager.AuthenticatorTypes private final int mAllowedAuthenticators;
// Prevent direct instantiation.
@SuppressWarnings("WeakerAccess") /* synthetic access */
PromptInfo(
@NonNull CharSequence title,
@Nullable CharSequence subtitle,
@Nullable CharSequence description,
@Nullable CharSequence negativeButtonText,
boolean confirmationRequired,
boolean deviceCredentialAllowed,
@BiometricManager.AuthenticatorTypes int allowedAuthenticators) {
mTitle = title;
mSubtitle = subtitle;
mDescription = description;
mNegativeButtonText = negativeButtonText;
mIsConfirmationRequired = confirmationRequired;
mIsDeviceCredentialAllowed = deviceCredentialAllowed;
mAllowedAuthenticators = allowedAuthenticators;
}
/**
* Gets the title for the prompt.
*
* @return The title to be displayed on the prompt.
*
* @see Builder#setTitle(CharSequence)
*/
@NonNull
public CharSequence getTitle() {
return mTitle;
}
/**
* Gets the subtitle for the prompt.
*
* @return The subtitle to be displayed on the prompt.
*
* @see Builder#setSubtitle(CharSequence)
*/
@Nullable
public CharSequence getSubtitle() {
return mSubtitle;
}
/**
* Gets the description for the prompt.
*
* @return The description to be displayed on the prompt.
*
* @see Builder#setDescription(CharSequence)
*/
@Nullable
public CharSequence getDescription() {
return mDescription;
}
/**
* Gets the text for the negative button on the prompt.
*
* @return The label to be used for the negative button on the prompt, or an empty string if
* not set.
*
* @see Builder#setNegativeButtonText(CharSequence)
*/
@NonNull
public CharSequence getNegativeButtonText() {
return mNegativeButtonText != null ? mNegativeButtonText : "";
}
/**
* Checks if the confirmation required option is enabled for the prompt.
*
* @return Whether this option is enabled.
*
* @see Builder#setConfirmationRequired(boolean)
*/
public boolean isConfirmationRequired() {
return mIsConfirmationRequired;
}
/**
* Checks if the device credential allowed option is enabled for the prompt.
*
* @return Whether this option is enabled.
*
* @see Builder#setDeviceCredentialAllowed(boolean)
*
* @deprecated Will be removed with {@link Builder#setDeviceCredentialAllowed(boolean)}.
*/
@SuppressWarnings({"deprecation", "DeprecatedIsStillUsed"})
@Deprecated
public boolean isDeviceCredentialAllowed() {
return mIsDeviceCredentialAllowed;
}
/**
* Gets the type(s) of authenticators that may be invoked by the prompt.
*
* @return A bit field representing all valid authenticator types that may be invoked by
* the prompt, or 0 if not set.
*
* @see Builder#setAllowedAuthenticators(int)
*/
@BiometricManager.AuthenticatorTypes
public int getAllowedAuthenticators() {
return mAllowedAuthenticators;
}
}
/**
* The fragment manager that will be used to attach the prompt to the client activity.
*/
@Nullable private FragmentManager mClientFragmentManager;
/**
* Constructs a {@link BiometricPrompt}, which can be used to prompt the user to authenticate
* with a biometric such as fingerprint or face. The prompt can be shown to the user by calling
* {@code authenticate()} and persists across device configuration changes by default.
*
* <p>If authentication is in progress, calling this constructor to recreate the prompt will
* also update the {@link AuthenticationCallback} for the current session. Thus, this method
* should be called by the client activity each time the configuration changes
* (e.g. in {@code onCreate()}).
*
* @param activity The activity of the client application that will host the prompt.
* @param callback The object that will receive and process authentication events.
*
* @see #BiometricPrompt(Fragment, AuthenticationCallback)
* @see #BiometricPrompt(FragmentActivity, Executor, AuthenticationCallback)
* @see #BiometricPrompt(Fragment, Executor, AuthenticationCallback)
*/
@SuppressWarnings("ConstantConditions")
public BiometricPrompt(
@NonNull FragmentActivity activity, @NonNull AuthenticationCallback callback) {
if (activity == null) {
throw new IllegalArgumentException("FragmentActivity must not be null.");
}
if (callback == null) {
throw new IllegalArgumentException("AuthenticationCallback must not be null.");
}
final FragmentManager fragmentManager = activity.getSupportFragmentManager();
init(activity, fragmentManager, null /* executor */, callback);
}
/**
* Constructs a {@link BiometricPrompt}, which can be used to prompt the user to authenticate
* with a biometric such as fingerprint or face. The prompt can be shown to the user by calling
* {@code authenticate()} and persists across device configuration changes by default.
*
* <p>If authentication is in progress, calling this constructor to recreate the prompt will
* also update the {@link AuthenticationCallback} for the current session. Thus, this method
* should be called by the client fragment each time the configuration changes
* (e.g. in {@code onCreate()}).
*
* @param fragment The fragment of the client application that will host the prompt.
* @param callback The object that will receive and process authentication events.
*
* @see #BiometricPrompt(FragmentActivity, AuthenticationCallback)
* @see #BiometricPrompt(FragmentActivity, Executor, AuthenticationCallback)
* @see #BiometricPrompt(Fragment, Executor, AuthenticationCallback)
*/
@SuppressWarnings("ConstantConditions")
public BiometricPrompt(@NonNull Fragment fragment, @NonNull AuthenticationCallback callback) {
if (fragment == null) {
throw new IllegalArgumentException("Fragment must not be null.");
}
if (callback == null) {
throw new IllegalArgumentException("AuthenticationCallback must not be null.");
}
final FragmentActivity activity = fragment.getActivity();
final FragmentManager fragmentManager = fragment.getChildFragmentManager();
init(activity, fragmentManager, null /* executor */, callback);
}
/**
* Constructs a {@link BiometricPrompt}, which can be used to prompt the user to authenticate
* with a biometric such as fingerprint or face. The prompt can be shown to the user by calling
* {@code authenticate()} and persists across device configuration changes by default.
*
* <p>If authentication is in progress, calling this constructor to recreate the prompt will
* also update the {@link Executor} and {@link AuthenticationCallback} for the current session.
* Thus, this method should be called by the client activity each time the configuration changes
* (e.g. in {@code onCreate()}).
*
* @param activity The activity of the client application that will host the prompt.
* @param executor The executor that will be used to run {@link AuthenticationCallback} methods.
* @param callback The object that will receive and process authentication events.
*
* @see #BiometricPrompt(FragmentActivity, AuthenticationCallback)
* @see #BiometricPrompt(Fragment, AuthenticationCallback)
* @see #BiometricPrompt(Fragment, Executor, AuthenticationCallback)
*/
@SuppressLint("LambdaLast")
@SuppressWarnings("ConstantConditions")
public BiometricPrompt(
@NonNull FragmentActivity activity,
@NonNull Executor executor,
@NonNull AuthenticationCallback callback) {
if (activity == null) {
throw new IllegalArgumentException("FragmentActivity must not be null.");
}
if (executor == null) {
throw new IllegalArgumentException("Executor must not be null.");
}
if (callback == null) {
throw new IllegalArgumentException("AuthenticationCallback must not be null.");
}
final FragmentManager fragmentManager = activity.getSupportFragmentManager();
init(activity, fragmentManager, executor, callback);
}
/**
* Constructs a {@link BiometricPrompt}, which can be used to prompt the user to authenticate
* with a biometric such as fingerprint or face. The prompt can be shown to the user by calling
* {@code authenticate()} and persists across device configuration changes by default.
*
* <p>If authentication is in progress, calling this constructor to recreate the prompt will
* also update the {@link Executor} and {@link AuthenticationCallback} for the current session.
* Thus, this method should be called by the client fragment each time the configuration changes
* (e.g. in {@code onCreate()}).
*
* @param fragment The fragment of the client application that will host the prompt.
* @param executor The executor that will be used to run {@link AuthenticationCallback} methods.
* @param callback The object that will receive and process authentication events.
*
* @see #BiometricPrompt(FragmentActivity, AuthenticationCallback)
* @see #BiometricPrompt(Fragment, AuthenticationCallback)
* @see #BiometricPrompt(FragmentActivity, Executor, AuthenticationCallback)
*/
@SuppressLint("LambdaLast")
@SuppressWarnings("ConstantConditions")
public BiometricPrompt(
@NonNull Fragment fragment,
@NonNull Executor executor,
@NonNull AuthenticationCallback callback) {
if (fragment == null) {
throw new IllegalArgumentException("Fragment must not be null.");
}
if (executor == null) {
throw new IllegalArgumentException("Executor must not be null.");
}
if (callback == null) {
throw new IllegalArgumentException("AuthenticationCallback must not be null.");
}
final FragmentActivity activity = fragment.getActivity();
final FragmentManager fragmentManager = fragment.getChildFragmentManager();
init(activity, fragmentManager, executor, callback);
}
/**
* Initializes or updates the data needed by the prompt.
*
* @param activity The client activity that will host the prompt.
* @param fragmentManager The fragment manager that will be used to attach the prompt.
* @param executor The executor that will be used to run callback methods, or
* {@link null} if a default executor should be used.
* @param callback The object that will receive and process authentication events.
*/
private void init(
@Nullable FragmentActivity activity,
@Nullable FragmentManager fragmentManager,
@Nullable Executor executor,
@NonNull AuthenticationCallback callback) {
mClientFragmentManager = fragmentManager;
if (activity != null) {
final BiometricViewModel viewModel =
new ViewModelProvider(activity).get(BiometricViewModel.class);
if (executor != null) {
viewModel.setClientExecutor(executor);
}
viewModel.setClientCallback(callback);
}
}
/**
* Shows the biometric prompt to the user. The prompt survives lifecycle changes by default. To
* cancel authentication and dismiss the prompt, use {@link #cancelAuthentication()}.
*
* <p>Calling this method invokes crypto-based authentication, which is incompatible with
* <strong>Class 2</strong> (formerly <strong>Weak</strong>) biometrics and (prior to Android
* 11) device credential. Therefore, it is an error for {@code info} to explicitly allow any
* of these authenticator types on an incompatible Android version.
*
* @param info An object describing the appearance and behavior of the prompt.
* @param crypto A crypto object to be associated with this authentication.
*
* @throws IllegalArgumentException If any of the allowed authenticator types specified by
* {@code info} do not support crypto-based authentication.
*
* @see #authenticate(PromptInfo)
* @see PromptInfo.Builder#setAllowedAuthenticators(int)
*/
@SuppressWarnings("ConstantConditions")
public void authenticate(@NonNull PromptInfo info, @NonNull CryptoObject crypto) {
if (info == null) {
throw new IllegalArgumentException("PromptInfo cannot be null.");
}
if (crypto == null) {
throw new IllegalArgumentException("CryptoObject cannot be null.");
}
// Ensure that all allowed authenticators support crypto auth.
@BiometricManager.AuthenticatorTypes final int authenticators =
AuthenticatorUtils.getConsolidatedAuthenticators(info, crypto);
if (AuthenticatorUtils.isWeakBiometricAllowed(authenticators)) {
throw new IllegalArgumentException("Crypto-based authentication is not supported for "
+ "Class 2 (Weak) biometrics.");
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R
&& AuthenticatorUtils.isDeviceCredentialAllowed(authenticators)) {
throw new IllegalArgumentException("Crypto-based authentication is not supported for "
+ "device credential prior to API 30.");
}
authenticateInternal(info, crypto);
}
/**
* Shows the biometric prompt to the user. The prompt survives lifecycle changes by default. To
* cancel authentication and dismiss the prompt, use {@link #cancelAuthentication()}.
*
* @param info An object describing the appearance and behavior of the prompt.
*
* @see #authenticate(PromptInfo, CryptoObject)
*/
@SuppressWarnings("ConstantConditions")
public void authenticate(@NonNull PromptInfo info) {
if (info == null) {
throw new IllegalArgumentException("PromptInfo cannot be null.");
}
authenticateInternal(info, null /* crypto */);
}
/**
* Shows the biometric prompt to the user and begins authentication.
*
* @param info An object describing the appearance and behavior of the prompt.
* @param crypto A crypto object to be associated with this authentication.
*/
private void authenticateInternal(@NonNull PromptInfo info, @Nullable CryptoObject crypto) {
if (mClientFragmentManager == null) {
Log.e(TAG, "Unable to start authentication. Client fragment manager was null.");
return;
}
if (mClientFragmentManager.isStateSaved()) {
Log.e(TAG, "Unable to start authentication. Called after onSaveInstanceState().");
return;
}
final BiometricFragment biometricFragment =
findOrAddBiometricFragment(mClientFragmentManager);
biometricFragment.authenticate(info, crypto);
}
/**
* Cancels the ongoing authentication session and dismisses the prompt.
*
* <p>On versions prior to Android 10 (API 29), calling this method while the user is
* authenticating with their device credential will NOT work as expected. See
* {@link PromptInfo.Builder#setDeviceCredentialAllowed(boolean)} for more details.
*/
public void cancelAuthentication() {
if (mClientFragmentManager == null) {
Log.e(TAG, "Unable to start authentication. Client fragment manager was null.");
return;
}
final BiometricFragment biometricFragment = findBiometricFragment(mClientFragmentManager);
if (biometricFragment == null) {
Log.e(TAG, "Unable to cancel authentication. BiometricFragment not found.");
return;
}
biometricFragment.cancelAuthentication(BiometricFragment.CANCELED_FROM_CLIENT);
}
/**
* Searches for a {@link BiometricFragment} instance that has been added to an activity or
* fragment.
*
* @param fragmentManager The fragment manager that will be used to search for the fragment.
* @return An instance of {@link BiometricFragment} found by the fragment manager, or
* {@code null} if no such fragment is found.
*/
@Nullable
private static BiometricFragment findBiometricFragment(
@NonNull FragmentManager fragmentManager) {
return (BiometricFragment) fragmentManager.findFragmentByTag(
BiometricPrompt.BIOMETRIC_FRAGMENT_TAG);
}
/**
* Returns a {@link BiometricFragment} instance that has been added to an activity or fragment,
* adding one if necessary.
*
* @param fragmentManager The fragment manager used to search for and/or add the fragment.
* @return An instance of {@link BiometricFragment} associated with the fragment manager.
*/
@NonNull
private static BiometricFragment findOrAddBiometricFragment(
@NonNull FragmentManager fragmentManager) {
BiometricFragment biometricFragment = findBiometricFragment(fragmentManager);
// If the fragment hasn't been added before, add it.
if (biometricFragment == null) {
biometricFragment = BiometricFragment.newInstance();
fragmentManager.beginTransaction()
.add(biometricFragment, BiometricPrompt.BIOMETRIC_FRAGMENT_TAG)
.commitAllowingStateLoss();
// For the case when onResume() is being called right after authenticate,
// we need to make sure that all fragment transactions have been committed.
fragmentManager.executePendingTransactions();
}
return biometricFragment;
}
}