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