blob: 1c9de457e9512fcb5ac93d36e401492f66c359a9 [file] [log] [blame]
/*
* Copyright (C) 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 android.hardware.biometrics;
import static android.Manifest.permission.USE_BIOMETRIC;
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.text.TextUtils;
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 dialog.
*/
public class BiometricPrompt implements BiometricAuthenticator, BiometricConstants {
/**
* @hide
*/
public static final String KEY_TITLE = "title";
/**
* @hide
*/
public static final String KEY_SUBTITLE = "subtitle";
/**
* @hide
*/
public static final String KEY_DESCRIPTION = "description";
/**
* @hide
*/
public static final String KEY_POSITIVE_TEXT = "positive_text";
/**
* @hide
*/
public static final String KEY_NEGATIVE_TEXT = "negative_text";
/**
* Error/help message will show for this amount of time.
* For error messages, the dialog will also be dismissed after this amount of time.
* Error messages will be propagated back to the application via AuthenticationCallback
* after this amount of time.
* @hide
*/
public static final int HIDE_DIALOG_DELAY = 2000; // ms
/**
* @hide
*/
public static final int DISMISSED_REASON_POSITIVE = 1;
/**
* @hide
*/
public static final int DISMISSED_REASON_NEGATIVE = 2;
/**
* @hide
*/
public static final int DISMISSED_REASON_USER_CANCEL = 3;
private static class ButtonInfo {
Executor executor;
DialogInterface.OnClickListener listener;
ButtonInfo(Executor ex, DialogInterface.OnClickListener l) {
executor = ex;
listener = l;
}
}
/**
* A builder that collects arguments to be shown on the system-provided biometric dialog.
**/
public static class Builder {
private final Bundle mBundle;
private ButtonInfo mPositiveButtonInfo;
private ButtonInfo mNegativeButtonInfo;
private Context mContext;
/**
* Creates a builder for a biometric dialog.
* @param context
*/
public Builder(Context context) {
mBundle = new Bundle();
mContext = context;
}
/**
* Required: Set the title to display.
* @param title
* @return
*/
public Builder setTitle(@NonNull CharSequence title) {
mBundle.putCharSequence(KEY_TITLE, title);
return this;
}
/**
* Optional: Set the subtitle to display.
* @param subtitle
* @return
*/
public Builder setSubtitle(@NonNull CharSequence subtitle) {
mBundle.putCharSequence(KEY_SUBTITLE, subtitle);
return this;
}
/**
* Optional: Set the description to display.
* @param description
* @return
*/
public Builder setDescription(@NonNull CharSequence description) {
mBundle.putCharSequence(KEY_DESCRIPTION, description);
return this;
}
/**
* Optional: Set the text for the positive button. If not set, the positive button
* will not show.
* @param text
* @return
* @hide
*/
public Builder setPositiveButton(@NonNull CharSequence text,
@NonNull @CallbackExecutor Executor executor,
@NonNull DialogInterface.OnClickListener listener) {
if (TextUtils.isEmpty(text)) {
throw new IllegalArgumentException("Text must be set and non-empty");
}
if (executor == null) {
throw new IllegalArgumentException("Executor must not be null");
}
if (listener == null) {
throw new IllegalArgumentException("Listener must not be null");
}
mBundle.putCharSequence(KEY_POSITIVE_TEXT, text);
mPositiveButtonInfo = new ButtonInfo(executor, listener);
return this;
}
/**
* Required: Set the text for the negative button. This would typically be used as a
* "Cancel" button, but may be also used to show an alternative method for authentication,
* such as screen that asks for a backup password.
* @param text
* @return
*/
public Builder setNegativeButton(@NonNull CharSequence text,
@NonNull @CallbackExecutor Executor executor,
@NonNull DialogInterface.OnClickListener listener) {
if (TextUtils.isEmpty(text)) {
throw new IllegalArgumentException("Text must be set and non-empty");
}
if (executor == null) {
throw new IllegalArgumentException("Executor must not be null");
}
if (listener == null) {
throw new IllegalArgumentException("Listener must not be null");
}
mBundle.putCharSequence(KEY_NEGATIVE_TEXT, text);
mNegativeButtonInfo = new ButtonInfo(executor, listener);
return this;
}
/**
* Creates a {@link BiometricPrompt}.
* @return a {@link BiometricPrompt}
* @throws IllegalArgumentException if any of the required fields are not set.
*/
public BiometricPrompt build() {
final CharSequence title = mBundle.getCharSequence(KEY_TITLE);
final CharSequence negative = mBundle.getCharSequence(KEY_NEGATIVE_TEXT);
if (TextUtils.isEmpty(title)) {
throw new IllegalArgumentException("Title must be set and non-empty");
} else if (TextUtils.isEmpty(negative)) {
throw new IllegalArgumentException("Negative text must be set and non-empty");
}
return new BiometricPrompt(mContext, mBundle, mPositiveButtonInfo, mNegativeButtonInfo);
}
}
private PackageManager mPackageManager;
private FingerprintManager mFingerprintManager;
private Bundle mBundle;
private ButtonInfo mPositiveButtonInfo;
private ButtonInfo mNegativeButtonInfo;
IBiometricPromptReceiver mDialogReceiver = new IBiometricPromptReceiver.Stub() {
@Override
public void onDialogDismissed(int reason) {
// Check the reason and invoke OnClickListener(s) if necessary
if (reason == DISMISSED_REASON_POSITIVE) {
mPositiveButtonInfo.executor.execute(() -> {
mPositiveButtonInfo.listener.onClick(null, DialogInterface.BUTTON_POSITIVE);
});
} else if (reason == DISMISSED_REASON_NEGATIVE) {
mNegativeButtonInfo.executor.execute(() -> {
mNegativeButtonInfo.listener.onClick(null, DialogInterface.BUTTON_NEGATIVE);
});
}
}
};
private BiometricPrompt(Context context, Bundle bundle,
ButtonInfo positiveButtonInfo, ButtonInfo negativeButtonInfo) {
mBundle = bundle;
mPositiveButtonInfo = positiveButtonInfo;
mNegativeButtonInfo = negativeButtonInfo;
mFingerprintManager = context.getSystemService(FingerprintManager.class);
mPackageManager = context.getPackageManager();
}
/**
* A wrapper class for the crypto objects supported by BiometricPrompt. Currently the framework
* supports {@link Signature}, {@link Cipher} and {@link Mac} objects.
*/
public static final class CryptoObject extends android.hardware.biometrics.CryptoObject {
public CryptoObject(@NonNull Signature signature) {
super(signature);
}
public CryptoObject(@NonNull Cipher cipher) {
super(cipher);
}
public CryptoObject(@NonNull Mac mac) {
super(mac);
}
/**
* Get {@link Signature} object.
* @return {@link Signature} object or null if this doesn't contain one.
*/
public Signature getSignature() {
return super.getSignature();
}
/**
* Get {@link Cipher} object.
* @return {@link Cipher} object or null if this doesn't contain one.
*/
public Cipher getCipher() {
return super.getCipher();
}
/**
* Get {@link Mac} object.
* @return {@link Mac} object or null if this doesn't contain one.
*/
public Mac getMac() {
return super.getMac();
}
}
/**
* Container for callback data from {@link #authenticate( CancellationSignal, Executor,
* AuthenticationCallback)} and {@link #authenticate(CryptoObject, CancellationSignal, Executor,
* AuthenticationCallback)}
*/
public static class AuthenticationResult extends BiometricAuthenticator.AuthenticationResult {
/**
* Authentication result
* @param crypto
* @param identifier
* @param userId
* @hide
*/
public AuthenticationResult(CryptoObject crypto, BiometricIdentifier identifier,
int userId) {
super(crypto, identifier, userId);
}
/**
* Obtain the crypto object associated with this transaction
* @return crypto object provided to {@link #authenticate( CryptoObject, CancellationSignal,
* Executor, AuthenticationCallback)}
*/
public CryptoObject getCryptoObject() {
return (CryptoObject) super.getCryptoObject();
}
}
/**
* Callback structure provided to {@link BiometricPrompt#authenticate(CancellationSignal,
* Executor, AuthenticationCallback)} or {@link BiometricPrompt#authenticate(CryptoObject,
* CancellationSignal, Executor, AuthenticationCallback)}. Users must provide an implementation
* of this for listening to authentication events.
*/
public abstract static class AuthenticationCallback extends
BiometricAuthenticator.AuthenticationCallback {
/**
* Called when an unrecoverable error has been encountered and the operation is complete.
* No further actions will be made on this object.
* @param errorCode An integer identifying the error message
* @param errString A human-readable error string that can be shown on an UI
*/
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {}
/**
* Called when a recoverable error has been encountered during authentication. The help
* string is provided to give the user guidance for what went wrong, such as "Sensor dirty,
* please clean it."
* @param helpCode An integer identifying the error message
* @param helpString A human-readable string that can be shown on an UI
*/
@Override
public void onAuthenticationHelp(int helpCode, CharSequence helpString) {}
/**
* Called when a biometric is recognized.
* @param result An object containing authentication-related data
*/
public void onAuthenticationSucceeded(AuthenticationResult result) {}
/**
* Called when a biometric is valid but not recognized.
*/
@Override
public void onAuthenticationFailed() {}
/**
* Called when a biometric has been acquired, but hasn't been processed yet.
* @hide
*/
@Override
public void onAuthenticationAcquired(int acquireInfo) {}
/**
* @param result An object containing authentication-related data
* @hide
*/
@Override
public void onAuthenticationSucceeded(BiometricAuthenticator.AuthenticationResult result) {
onAuthenticationSucceeded(new AuthenticationResult(
(CryptoObject) result.getCryptoObject(),
result.getId(),
result.getUserId()));
}
}
/**
* @param crypto Object associated with the call
* @param cancel An object that can be used to cancel authentication
* @param executor An executor to handle callback events
* @param callback An object to receive authentication events
* @hide
*/
@Override
public void authenticate(@NonNull android.hardware.biometrics.CryptoObject crypto,
@NonNull CancellationSignal cancel,
@NonNull @CallbackExecutor Executor executor,
@NonNull BiometricAuthenticator.AuthenticationCallback callback) {
if (!(callback instanceof BiometricPrompt.AuthenticationCallback)) {
throw new IllegalArgumentException("Callback cannot be casted");
}
authenticate(crypto, cancel, executor, (AuthenticationCallback) callback);
}
/**
*
* @param cancel An object that can be used to cancel authentication
* @param executor An executor to handle callback events
* @param callback An object to receive authentication events
* @hide
*/
@Override
public void authenticate(@NonNull CancellationSignal cancel,
@NonNull @CallbackExecutor Executor executor,
@NonNull BiometricAuthenticator.AuthenticationCallback callback) {
if (!(callback instanceof BiometricPrompt.AuthenticationCallback)) {
throw new IllegalArgumentException("Callback cannot be casted");
}
authenticate(cancel, executor, (AuthenticationCallback) callback);
}
/**
* This call warms up the fingerprint hardware, displays a system-provided dialog, and starts
* scanning for a fingerprint. It terminates when {@link
* AuthenticationCallback#onAuthenticationError(int, CharSequence)} is called, when {@link
* AuthenticationCallback#onAuthenticationSucceeded( AuthenticationResult)}, or when the user
* dismisses the system-provided dialog, at which point the crypto object becomes invalid. This
* operation can be canceled by using the provided cancel object. The application will receive
* authentication errors through {@link AuthenticationCallback}, and button events through the
* corresponding callback set in {@link Builder#setNegativeButton(CharSequence, Executor,
* DialogInterface.OnClickListener)}. It is safe to reuse the {@link BiometricPrompt} object,
* and calling {@link BiometricPrompt#authenticate( CancellationSignal, Executor,
* AuthenticationCallback)} while an existing authentication attempt is occurring will stop the
* previous client and start a new authentication. The interrupted client will receive a
* cancelled notification through {@link AuthenticationCallback#onAuthenticationError(int,
* CharSequence)}.
*
* @throws IllegalArgumentException If any of the arguments are null
*
* @param crypto Object associated with the call
* @param cancel An object that can be used to cancel authentication
* @param executor An executor to handle callback events
* @param callback An object to receive authentication events
*/
@RequiresPermission(USE_BIOMETRIC)
public void authenticate(@NonNull CryptoObject crypto,
@NonNull CancellationSignal cancel,
@NonNull @CallbackExecutor Executor executor,
@NonNull AuthenticationCallback callback) {
if (handlePreAuthenticationErrors(callback, executor)) {
return;
}
mFingerprintManager.authenticate(crypto, cancel, mBundle, executor, mDialogReceiver,
callback);
}
/**
* This call warms up the fingerprint hardware, displays a system-provided dialog, and starts
* scanning for a fingerprint. It terminates when {@link
* AuthenticationCallback#onAuthenticationError(int, CharSequence)} is called, when {@link
* AuthenticationCallback#onAuthenticationSucceeded( AuthenticationResult)} is called, or when
* the user dismisses the system-provided dialog. This operation can be canceled by using the
* provided cancel object. The application will receive authentication errors through {@link
* AuthenticationCallback}, and button events through the corresponding callback set in {@link
* Builder#setNegativeButton(CharSequence, Executor, DialogInterface.OnClickListener)}. It is
* safe to reuse the {@link BiometricPrompt} object, and calling {@link
* BiometricPrompt#authenticate(CancellationSignal, Executor, AuthenticationCallback)} while
* an existing authentication attempt is occurring will stop the previous client and start a new
* authentication. The interrupted client will receive a cancelled notification through {@link
* AuthenticationCallback#onAuthenticationError(int, CharSequence)}.
*
* @throws IllegalArgumentException If any of the arguments are null
*
* @param cancel An object that can be used to cancel authentication
* @param executor An executor to handle callback events
* @param callback An object to receive authentication events
*/
@RequiresPermission(USE_BIOMETRIC)
public void authenticate(@NonNull CancellationSignal cancel,
@NonNull @CallbackExecutor Executor executor,
@NonNull AuthenticationCallback callback) {
if (handlePreAuthenticationErrors(callback, executor)) {
return;
}
mFingerprintManager.authenticate(cancel, mBundle, executor, mDialogReceiver, callback);
}
private boolean handlePreAuthenticationErrors(AuthenticationCallback callback,
Executor executor) {
if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
sendError(BiometricPrompt.BIOMETRIC_ERROR_HW_NOT_PRESENT, callback,
executor);
return true;
} else if (!mFingerprintManager.isHardwareDetected()) {
sendError(BiometricPrompt.BIOMETRIC_ERROR_HW_UNAVAILABLE, callback,
executor);
return true;
} else if (!mFingerprintManager.hasEnrolledFingerprints()) {
sendError(BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS, callback,
executor);
return true;
}
return false;
}
private void sendError(int error, AuthenticationCallback callback, Executor executor) {
executor.execute(() -> {
callback.onAuthenticationError(error, mFingerprintManager.getErrorString(
error, 0 /* vendorCode */));
});
}
}