diff options
Diffstat (limited to 'android/hardware/biometrics/BiometricPrompt.java')
-rw-r--r-- | android/hardware/biometrics/BiometricPrompt.java | 494 |
1 files changed, 494 insertions, 0 deletions
diff --git a/android/hardware/biometrics/BiometricPrompt.java b/android/hardware/biometrics/BiometricPrompt.java new file mode 100644 index 00000000..1c9de457 --- /dev/null +++ b/android/hardware/biometrics/BiometricPrompt.java @@ -0,0 +1,494 @@ +/* + * 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 */)); + }); + } +} |