1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.hardware.biometrics;
18 
19 import static android.Manifest.permission.USE_BIOMETRIC;
20 
21 import android.annotation.CallbackExecutor;
22 import android.annotation.NonNull;
23 import android.annotation.RequiresPermission;
24 import android.content.Context;
25 import android.content.DialogInterface;
26 import android.content.pm.PackageManager;
27 import android.hardware.fingerprint.FingerprintManager;
28 import android.os.Bundle;
29 import android.os.CancellationSignal;
30 import android.text.TextUtils;
31 
32 import java.security.Signature;
33 import java.util.concurrent.Executor;
34 
35 import javax.crypto.Cipher;
36 import javax.crypto.Mac;
37 
38 /**
39  * A class that manages a system-provided biometric dialog.
40  */
41 public class BiometricPrompt implements BiometricAuthenticator, BiometricConstants {
42 
43     /**
44      * @hide
45      */
46     public static final String KEY_TITLE = "title";
47     /**
48      * @hide
49      */
50     public static final String KEY_SUBTITLE = "subtitle";
51     /**
52      * @hide
53      */
54     public static final String KEY_DESCRIPTION = "description";
55     /**
56      * @hide
57      */
58     public static final String KEY_POSITIVE_TEXT = "positive_text";
59     /**
60      * @hide
61      */
62     public static final String KEY_NEGATIVE_TEXT = "negative_text";
63 
64     /**
65      * Error/help message will show for this amount of time.
66      * For error messages, the dialog will also be dismissed after this amount of time.
67      * Error messages will be propagated back to the application via AuthenticationCallback
68      * after this amount of time.
69      * @hide
70      */
71     public static final int HIDE_DIALOG_DELAY = 2000; // ms
72     /**
73      * @hide
74      */
75     public static final int DISMISSED_REASON_POSITIVE = 1;
76 
77     /**
78      * @hide
79      */
80     public static final int DISMISSED_REASON_NEGATIVE = 2;
81 
82     /**
83      * @hide
84      */
85     public static final int DISMISSED_REASON_USER_CANCEL = 3;
86 
87     private static class ButtonInfo {
88         Executor executor;
89         DialogInterface.OnClickListener listener;
ButtonInfo(Executor ex, DialogInterface.OnClickListener l)90         ButtonInfo(Executor ex, DialogInterface.OnClickListener l) {
91             executor = ex;
92             listener = l;
93         }
94     }
95 
96     /**
97      * A builder that collects arguments to be shown on the system-provided biometric dialog.
98      **/
99     public static class Builder {
100         private final Bundle mBundle;
101         private ButtonInfo mPositiveButtonInfo;
102         private ButtonInfo mNegativeButtonInfo;
103         private Context mContext;
104 
105         /**
106          * Creates a builder for a biometric dialog.
107          * @param context
108          */
Builder(Context context)109         public Builder(Context context) {
110             mBundle = new Bundle();
111             mContext = context;
112         }
113 
114         /**
115          * Required: Set the title to display.
116          * @param title
117          * @return
118          */
setTitle(@onNull CharSequence title)119         public Builder setTitle(@NonNull CharSequence title) {
120             mBundle.putCharSequence(KEY_TITLE, title);
121             return this;
122         }
123 
124         /**
125          * Optional: Set the subtitle to display.
126          * @param subtitle
127          * @return
128          */
setSubtitle(@onNull CharSequence subtitle)129         public Builder setSubtitle(@NonNull CharSequence subtitle) {
130             mBundle.putCharSequence(KEY_SUBTITLE, subtitle);
131             return this;
132         }
133 
134         /**
135          * Optional: Set the description to display.
136          * @param description
137          * @return
138          */
setDescription(@onNull CharSequence description)139         public Builder setDescription(@NonNull CharSequence description) {
140             mBundle.putCharSequence(KEY_DESCRIPTION, description);
141             return this;
142         }
143 
144         /**
145          * Optional: Set the text for the positive button. If not set, the positive button
146          * will not show.
147          * @param text
148          * @return
149          * @hide
150          */
setPositiveButton(@onNull CharSequence text, @NonNull @CallbackExecutor Executor executor, @NonNull DialogInterface.OnClickListener listener)151         public Builder setPositiveButton(@NonNull CharSequence text,
152                 @NonNull @CallbackExecutor Executor executor,
153                 @NonNull DialogInterface.OnClickListener listener) {
154             if (TextUtils.isEmpty(text)) {
155                 throw new IllegalArgumentException("Text must be set and non-empty");
156             }
157             if (executor == null) {
158                 throw new IllegalArgumentException("Executor must not be null");
159             }
160             if (listener == null) {
161                 throw new IllegalArgumentException("Listener must not be null");
162             }
163             mBundle.putCharSequence(KEY_POSITIVE_TEXT, text);
164             mPositiveButtonInfo = new ButtonInfo(executor, listener);
165             return this;
166         }
167 
168         /**
169          * Required: Set the text for the negative button. This would typically be used as a
170          * "Cancel" button, but may be also used to show an alternative method for authentication,
171          * such as screen that asks for a backup password.
172          * @param text
173          * @return
174          */
setNegativeButton(@onNull CharSequence text, @NonNull @CallbackExecutor Executor executor, @NonNull DialogInterface.OnClickListener listener)175         public Builder setNegativeButton(@NonNull CharSequence text,
176                 @NonNull @CallbackExecutor Executor executor,
177                 @NonNull DialogInterface.OnClickListener listener) {
178             if (TextUtils.isEmpty(text)) {
179                 throw new IllegalArgumentException("Text must be set and non-empty");
180             }
181             if (executor == null) {
182                 throw new IllegalArgumentException("Executor must not be null");
183             }
184             if (listener == null) {
185                 throw new IllegalArgumentException("Listener must not be null");
186             }
187             mBundle.putCharSequence(KEY_NEGATIVE_TEXT, text);
188             mNegativeButtonInfo = new ButtonInfo(executor, listener);
189             return this;
190         }
191 
192         /**
193          * Creates a {@link BiometricPrompt}.
194          * @return a {@link BiometricPrompt}
195          * @throws IllegalArgumentException if any of the required fields are not set.
196          */
build()197         public BiometricPrompt build() {
198             final CharSequence title = mBundle.getCharSequence(KEY_TITLE);
199             final CharSequence negative = mBundle.getCharSequence(KEY_NEGATIVE_TEXT);
200 
201             if (TextUtils.isEmpty(title)) {
202                 throw new IllegalArgumentException("Title must be set and non-empty");
203             } else if (TextUtils.isEmpty(negative)) {
204                 throw new IllegalArgumentException("Negative text must be set and non-empty");
205             }
206             return new BiometricPrompt(mContext, mBundle, mPositiveButtonInfo, mNegativeButtonInfo);
207         }
208     }
209 
210     private PackageManager mPackageManager;
211     private FingerprintManager mFingerprintManager;
212     private Bundle mBundle;
213     private ButtonInfo mPositiveButtonInfo;
214     private ButtonInfo mNegativeButtonInfo;
215 
216     IBiometricPromptReceiver mDialogReceiver = new IBiometricPromptReceiver.Stub() {
217         @Override
218         public void onDialogDismissed(int reason) {
219             // Check the reason and invoke OnClickListener(s) if necessary
220             if (reason == DISMISSED_REASON_POSITIVE) {
221                 mPositiveButtonInfo.executor.execute(() -> {
222                     mPositiveButtonInfo.listener.onClick(null, DialogInterface.BUTTON_POSITIVE);
223                 });
224             } else if (reason == DISMISSED_REASON_NEGATIVE) {
225                 mNegativeButtonInfo.executor.execute(() -> {
226                     mNegativeButtonInfo.listener.onClick(null, DialogInterface.BUTTON_NEGATIVE);
227                 });
228             }
229         }
230     };
231 
BiometricPrompt(Context context, Bundle bundle, ButtonInfo positiveButtonInfo, ButtonInfo negativeButtonInfo)232     private BiometricPrompt(Context context, Bundle bundle,
233             ButtonInfo positiveButtonInfo, ButtonInfo negativeButtonInfo) {
234         mBundle = bundle;
235         mPositiveButtonInfo = positiveButtonInfo;
236         mNegativeButtonInfo = negativeButtonInfo;
237         mFingerprintManager = context.getSystemService(FingerprintManager.class);
238         mPackageManager = context.getPackageManager();
239     }
240 
241     /**
242      * A wrapper class for the crypto objects supported by BiometricPrompt. Currently the framework
243      * supports {@link Signature}, {@link Cipher} and {@link Mac} objects.
244      */
245     public static final class CryptoObject extends android.hardware.biometrics.CryptoObject {
CryptoObject(@onNull Signature signature)246         public CryptoObject(@NonNull Signature signature) {
247             super(signature);
248         }
249 
CryptoObject(@onNull Cipher cipher)250         public CryptoObject(@NonNull Cipher cipher) {
251             super(cipher);
252         }
253 
CryptoObject(@onNull Mac mac)254         public CryptoObject(@NonNull Mac mac) {
255             super(mac);
256         }
257 
258         /**
259          * Get {@link Signature} object.
260          * @return {@link Signature} object or null if this doesn't contain one.
261          */
getSignature()262         public Signature getSignature() {
263             return super.getSignature();
264         }
265 
266         /**
267          * Get {@link Cipher} object.
268          * @return {@link Cipher} object or null if this doesn't contain one.
269          */
getCipher()270         public Cipher getCipher() {
271             return super.getCipher();
272         }
273 
274         /**
275          * Get {@link Mac} object.
276          * @return {@link Mac} object or null if this doesn't contain one.
277          */
getMac()278         public Mac getMac() {
279             return super.getMac();
280         }
281     }
282 
283     /**
284      * Container for callback data from {@link #authenticate( CancellationSignal, Executor,
285      * AuthenticationCallback)} and {@link #authenticate(CryptoObject, CancellationSignal, Executor,
286      * AuthenticationCallback)}
287      */
288     public static class AuthenticationResult extends BiometricAuthenticator.AuthenticationResult {
289         /**
290          * Authentication result
291          * @param crypto
292          * @param identifier
293          * @param userId
294          * @hide
295          */
AuthenticationResult(CryptoObject crypto, BiometricIdentifier identifier, int userId)296         public AuthenticationResult(CryptoObject crypto, BiometricIdentifier identifier,
297                 int userId) {
298             super(crypto, identifier, userId);
299         }
300         /**
301          * Obtain the crypto object associated with this transaction
302          * @return crypto object provided to {@link #authenticate( CryptoObject, CancellationSignal,
303          * Executor, AuthenticationCallback)}
304          */
getCryptoObject()305         public CryptoObject getCryptoObject() {
306             return (CryptoObject) super.getCryptoObject();
307         }
308     }
309 
310     /**
311      * Callback structure provided to {@link BiometricPrompt#authenticate(CancellationSignal,
312      * Executor, AuthenticationCallback)} or {@link BiometricPrompt#authenticate(CryptoObject,
313      * CancellationSignal, Executor, AuthenticationCallback)}. Users must provide an implementation
314      * of this for listening to authentication events.
315      */
316     public abstract static class AuthenticationCallback extends
317             BiometricAuthenticator.AuthenticationCallback {
318         /**
319          * Called when an unrecoverable error has been encountered and the operation is complete.
320          * No further actions will be made on this object.
321          * @param errorCode An integer identifying the error message
322          * @param errString A human-readable error string that can be shown on an UI
323          */
324         @Override
onAuthenticationError(int errorCode, CharSequence errString)325         public void onAuthenticationError(int errorCode, CharSequence errString) {}
326 
327         /**
328          * Called when a recoverable error has been encountered during authentication. The help
329          * string is provided to give the user guidance for what went wrong, such as "Sensor dirty,
330          * please clean it."
331          * @param helpCode An integer identifying the error message
332          * @param helpString A human-readable string that can be shown on an UI
333          */
334         @Override
onAuthenticationHelp(int helpCode, CharSequence helpString)335         public void onAuthenticationHelp(int helpCode, CharSequence helpString) {}
336 
337         /**
338          * Called when a biometric is recognized.
339          * @param result An object containing authentication-related data
340          */
onAuthenticationSucceeded(AuthenticationResult result)341         public void onAuthenticationSucceeded(AuthenticationResult result) {}
342 
343         /**
344          * Called when a biometric is valid but not recognized.
345          */
346         @Override
onAuthenticationFailed()347         public void onAuthenticationFailed() {}
348 
349         /**
350          * Called when a biometric has been acquired, but hasn't been processed yet.
351          * @hide
352          */
353         @Override
onAuthenticationAcquired(int acquireInfo)354         public void onAuthenticationAcquired(int acquireInfo) {}
355 
356         /**
357          * @param result An object containing authentication-related data
358          * @hide
359          */
360         @Override
onAuthenticationSucceeded(BiometricAuthenticator.AuthenticationResult result)361         public void onAuthenticationSucceeded(BiometricAuthenticator.AuthenticationResult result) {
362             onAuthenticationSucceeded(new AuthenticationResult(
363                     (CryptoObject) result.getCryptoObject(),
364                     result.getId(),
365                     result.getUserId()));
366         }
367     }
368 
369     /**
370      * @param crypto Object associated with the call
371      * @param cancel An object that can be used to cancel authentication
372      * @param executor An executor to handle callback events
373      * @param callback An object to receive authentication events
374      * @hide
375      */
376     @Override
authenticate(@onNull android.hardware.biometrics.CryptoObject crypto, @NonNull CancellationSignal cancel, @NonNull @CallbackExecutor Executor executor, @NonNull BiometricAuthenticator.AuthenticationCallback callback)377     public void authenticate(@NonNull android.hardware.biometrics.CryptoObject crypto,
378             @NonNull CancellationSignal cancel,
379             @NonNull @CallbackExecutor Executor executor,
380             @NonNull BiometricAuthenticator.AuthenticationCallback callback) {
381         if (!(callback instanceof BiometricPrompt.AuthenticationCallback)) {
382             throw new IllegalArgumentException("Callback cannot be casted");
383         }
384         authenticate(crypto, cancel, executor, (AuthenticationCallback) callback);
385     }
386 
387     /**
388      *
389      * @param cancel An object that can be used to cancel authentication
390      * @param executor An executor to handle callback events
391      * @param callback An object to receive authentication events
392      * @hide
393      */
394     @Override
authenticate(@onNull CancellationSignal cancel, @NonNull @CallbackExecutor Executor executor, @NonNull BiometricAuthenticator.AuthenticationCallback callback)395     public void authenticate(@NonNull CancellationSignal cancel,
396             @NonNull @CallbackExecutor Executor executor,
397             @NonNull BiometricAuthenticator.AuthenticationCallback callback) {
398         if (!(callback instanceof BiometricPrompt.AuthenticationCallback)) {
399             throw new IllegalArgumentException("Callback cannot be casted");
400         }
401         authenticate(cancel, executor, (AuthenticationCallback) callback);
402     }
403 
404     /**
405      * This call warms up the fingerprint hardware, displays a system-provided dialog, and starts
406      * scanning for a fingerprint. It terminates when {@link
407      * AuthenticationCallback#onAuthenticationError(int, CharSequence)} is called, when {@link
408      * AuthenticationCallback#onAuthenticationSucceeded( AuthenticationResult)}, or when the user
409      * dismisses the system-provided dialog, at which point the crypto object becomes invalid. This
410      * operation can be canceled by using the provided cancel object. The application will receive
411      * authentication errors through {@link AuthenticationCallback}, and button events through the
412      * corresponding callback set in {@link Builder#setNegativeButton(CharSequence, Executor,
413      * DialogInterface.OnClickListener)}. It is safe to reuse the {@link BiometricPrompt} object,
414      * and calling {@link BiometricPrompt#authenticate( CancellationSignal, Executor,
415      * AuthenticationCallback)} while an existing authentication attempt is occurring will stop the
416      * previous client and start a new authentication. The interrupted client will receive a
417      * cancelled notification through {@link AuthenticationCallback#onAuthenticationError(int,
418      * CharSequence)}.
419      *
420      * @throws IllegalArgumentException If any of the arguments are null
421      *
422      * @param crypto Object associated with the call
423      * @param cancel An object that can be used to cancel authentication
424      * @param executor An executor to handle callback events
425      * @param callback An object to receive authentication events
426      */
427     @RequiresPermission(USE_BIOMETRIC)
authenticate(@onNull CryptoObject crypto, @NonNull CancellationSignal cancel, @NonNull @CallbackExecutor Executor executor, @NonNull AuthenticationCallback callback)428     public void authenticate(@NonNull CryptoObject crypto,
429             @NonNull CancellationSignal cancel,
430             @NonNull @CallbackExecutor Executor executor,
431             @NonNull AuthenticationCallback callback) {
432         if (handlePreAuthenticationErrors(callback, executor)) {
433             return;
434         }
435         mFingerprintManager.authenticate(crypto, cancel, mBundle, executor, mDialogReceiver,
436                 callback);
437     }
438 
439     /**
440      * This call warms up the fingerprint hardware, displays a system-provided dialog, and starts
441      * scanning for a fingerprint. It terminates when {@link
442      * AuthenticationCallback#onAuthenticationError(int, CharSequence)} is called, when {@link
443      * AuthenticationCallback#onAuthenticationSucceeded( AuthenticationResult)} is called, or when
444      * the user dismisses the system-provided dialog.  This operation can be canceled by using the
445      * provided cancel object. The application will receive authentication errors through {@link
446      * AuthenticationCallback}, and button events through the corresponding callback set in {@link
447      * Builder#setNegativeButton(CharSequence, Executor, DialogInterface.OnClickListener)}.  It is
448      * safe to reuse the {@link BiometricPrompt} object, and calling {@link
449      * BiometricPrompt#authenticate(CancellationSignal, Executor, AuthenticationCallback)} while
450      * an existing authentication attempt is occurring will stop the previous client and start a new
451      * authentication. The interrupted client will receive a cancelled notification through {@link
452      * AuthenticationCallback#onAuthenticationError(int, CharSequence)}.
453      *
454      * @throws IllegalArgumentException If any of the arguments are null
455      *
456      * @param cancel An object that can be used to cancel authentication
457      * @param executor An executor to handle callback events
458      * @param callback An object to receive authentication events
459      */
460     @RequiresPermission(USE_BIOMETRIC)
authenticate(@onNull CancellationSignal cancel, @NonNull @CallbackExecutor Executor executor, @NonNull AuthenticationCallback callback)461     public void authenticate(@NonNull CancellationSignal cancel,
462             @NonNull @CallbackExecutor Executor executor,
463             @NonNull AuthenticationCallback callback) {
464         if (handlePreAuthenticationErrors(callback, executor)) {
465             return;
466         }
467         mFingerprintManager.authenticate(cancel, mBundle, executor, mDialogReceiver, callback);
468     }
469 
handlePreAuthenticationErrors(AuthenticationCallback callback, Executor executor)470     private boolean handlePreAuthenticationErrors(AuthenticationCallback callback,
471             Executor executor) {
472         if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
473             sendError(BiometricPrompt.BIOMETRIC_ERROR_HW_NOT_PRESENT, callback,
474                       executor);
475             return true;
476         } else if (!mFingerprintManager.isHardwareDetected()) {
477             sendError(BiometricPrompt.BIOMETRIC_ERROR_HW_UNAVAILABLE, callback,
478                       executor);
479             return true;
480         } else if (!mFingerprintManager.hasEnrolledFingerprints()) {
481             sendError(BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS, callback,
482                       executor);
483             return true;
484         }
485         return false;
486     }
487 
sendError(int error, AuthenticationCallback callback, Executor executor)488     private void sendError(int error, AuthenticationCallback callback, Executor executor) {
489         executor.execute(() -> {
490             callback.onAuthenticationError(error, mFingerprintManager.getErrorString(
491                     error, 0 /* vendorCode */));
492         });
493     }
494 }
495