Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 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 | |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 17 | package android.hardware.biometrics; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 18 | |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 19 | import static android.Manifest.permission.USE_BIOMETRIC; |
Kevin Chyn | 3a018719 | 2018-10-08 15:40:05 -0700 | [diff] [blame] | 20 | import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 21 | |
| 22 | import android.annotation.CallbackExecutor; |
| 23 | import android.annotation.NonNull; |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 24 | import android.annotation.Nullable; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 25 | import android.annotation.RequiresPermission; |
Kevin Chyn | 45d1f9d | 2019-02-07 13:38:04 -0800 | [diff] [blame] | 26 | import android.app.KeyguardManager; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 27 | import android.content.Context; |
| 28 | import android.content.DialogInterface; |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 29 | import android.os.Binder; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 30 | import android.os.Bundle; |
| 31 | import android.os.CancellationSignal; |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 32 | import android.os.IBinder; |
| 33 | import android.os.RemoteException; |
| 34 | import android.os.ServiceManager; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 35 | import android.text.TextUtils; |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 36 | import android.util.Log; |
| 37 | |
| 38 | import com.android.internal.R; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 39 | |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 40 | import java.security.Signature; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 41 | import java.util.concurrent.Executor; |
| 42 | |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 43 | import javax.crypto.Cipher; |
| 44 | import javax.crypto.Mac; |
| 45 | |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 46 | /** |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 47 | * A class that manages a system-provided biometric dialog. |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 48 | */ |
Vishwath Mohan | ecf00ce | 2018-04-05 10:28:24 -0700 | [diff] [blame] | 49 | public class BiometricPrompt implements BiometricAuthenticator, BiometricConstants { |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 50 | |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 51 | private static final String TAG = "BiometricPrompt"; |
| 52 | |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 53 | /** |
| 54 | * @hide |
| 55 | */ |
| 56 | public static final String KEY_TITLE = "title"; |
| 57 | /** |
| 58 | * @hide |
| 59 | */ |
Kevin Chyn | 3a018719 | 2018-10-08 15:40:05 -0700 | [diff] [blame] | 60 | public static final String KEY_USE_DEFAULT_TITLE = "use_default_title"; |
| 61 | /** |
| 62 | * @hide |
| 63 | */ |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 64 | public static final String KEY_SUBTITLE = "subtitle"; |
| 65 | /** |
| 66 | * @hide |
| 67 | */ |
| 68 | public static final String KEY_DESCRIPTION = "description"; |
| 69 | /** |
| 70 | * @hide |
| 71 | */ |
| 72 | public static final String KEY_POSITIVE_TEXT = "positive_text"; |
| 73 | /** |
| 74 | * @hide |
| 75 | */ |
| 76 | public static final String KEY_NEGATIVE_TEXT = "negative_text"; |
Kevin Chyn | 158fefb | 2019-01-03 18:59:05 -0800 | [diff] [blame] | 77 | /** |
| 78 | * @hide |
| 79 | */ |
| 80 | public static final String KEY_REQUIRE_CONFIRMATION = "require_confirmation"; |
Kevin Chyn | 1b2137c | 2019-01-24 16:32:38 -0800 | [diff] [blame] | 81 | /** |
| 82 | * @hide |
| 83 | */ |
Kevin Chyn | 45d1f9d | 2019-02-07 13:38:04 -0800 | [diff] [blame] | 84 | public static final String KEY_ALLOW_DEVICE_CREDENTIAL = "allow_device_credential"; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 85 | |
| 86 | /** |
| 87 | * Error/help message will show for this amount of time. |
| 88 | * For error messages, the dialog will also be dismissed after this amount of time. |
| 89 | * Error messages will be propagated back to the application via AuthenticationCallback |
| 90 | * after this amount of time. |
| 91 | * @hide |
| 92 | */ |
Kevin Chyn | dba919a | 2018-03-16 14:35:10 -0700 | [diff] [blame] | 93 | public static final int HIDE_DIALOG_DELAY = 2000; // ms |
Kevin Chyn | ed94180 | 2018-08-14 19:00:57 -0700 | [diff] [blame] | 94 | |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 95 | /** |
| 96 | * @hide |
| 97 | */ |
| 98 | public static final int DISMISSED_REASON_POSITIVE = 1; |
| 99 | |
| 100 | /** |
| 101 | * @hide |
| 102 | */ |
| 103 | public static final int DISMISSED_REASON_NEGATIVE = 2; |
| 104 | |
| 105 | /** |
| 106 | * @hide |
| 107 | */ |
| 108 | public static final int DISMISSED_REASON_USER_CANCEL = 3; |
| 109 | |
| 110 | private static class ButtonInfo { |
| 111 | Executor executor; |
| 112 | DialogInterface.OnClickListener listener; |
| 113 | ButtonInfo(Executor ex, DialogInterface.OnClickListener l) { |
| 114 | executor = ex; |
| 115 | listener = l; |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | /** |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 120 | * A builder that collects arguments to be shown on the system-provided biometric dialog. |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 121 | **/ |
| 122 | public static class Builder { |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 123 | private final Bundle mBundle; |
| 124 | private ButtonInfo mPositiveButtonInfo; |
| 125 | private ButtonInfo mNegativeButtonInfo; |
| 126 | private Context mContext; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 127 | |
| 128 | /** |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 129 | * Creates a builder for a biometric dialog. |
| 130 | * @param context |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 131 | */ |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 132 | public Builder(Context context) { |
| 133 | mBundle = new Bundle(); |
| 134 | mContext = context; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 135 | } |
| 136 | |
| 137 | /** |
| 138 | * Required: Set the title to display. |
| 139 | * @param title |
| 140 | * @return |
| 141 | */ |
| 142 | public Builder setTitle(@NonNull CharSequence title) { |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 143 | mBundle.putCharSequence(KEY_TITLE, title); |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 144 | return this; |
| 145 | } |
| 146 | |
| 147 | /** |
Kevin Chyn | 3a018719 | 2018-10-08 15:40:05 -0700 | [diff] [blame] | 148 | * For internal use currently. Only takes effect if title is null/empty. Shows a default |
| 149 | * modality-specific title. |
| 150 | * @hide |
| 151 | */ |
| 152 | @RequiresPermission(USE_BIOMETRIC_INTERNAL) |
| 153 | public Builder setUseDefaultTitle() { |
| 154 | mBundle.putBoolean(KEY_USE_DEFAULT_TITLE, true); |
| 155 | return this; |
| 156 | } |
| 157 | |
| 158 | /** |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 159 | * Optional: Set the subtitle to display. |
| 160 | * @param subtitle |
| 161 | * @return |
| 162 | */ |
| 163 | public Builder setSubtitle(@NonNull CharSequence subtitle) { |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 164 | mBundle.putCharSequence(KEY_SUBTITLE, subtitle); |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 165 | return this; |
| 166 | } |
| 167 | |
| 168 | /** |
| 169 | * Optional: Set the description to display. |
| 170 | * @param description |
| 171 | * @return |
| 172 | */ |
| 173 | public Builder setDescription(@NonNull CharSequence description) { |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 174 | mBundle.putCharSequence(KEY_DESCRIPTION, description); |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 175 | return this; |
| 176 | } |
| 177 | |
| 178 | /** |
| 179 | * Optional: Set the text for the positive button. If not set, the positive button |
| 180 | * will not show. |
| 181 | * @param text |
| 182 | * @return |
| 183 | * @hide |
| 184 | */ |
| 185 | public Builder setPositiveButton(@NonNull CharSequence text, |
| 186 | @NonNull @CallbackExecutor Executor executor, |
| 187 | @NonNull DialogInterface.OnClickListener listener) { |
| 188 | if (TextUtils.isEmpty(text)) { |
| 189 | throw new IllegalArgumentException("Text must be set and non-empty"); |
| 190 | } |
| 191 | if (executor == null) { |
| 192 | throw new IllegalArgumentException("Executor must not be null"); |
| 193 | } |
| 194 | if (listener == null) { |
| 195 | throw new IllegalArgumentException("Listener must not be null"); |
| 196 | } |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 197 | mBundle.putCharSequence(KEY_POSITIVE_TEXT, text); |
| 198 | mPositiveButtonInfo = new ButtonInfo(executor, listener); |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 199 | return this; |
| 200 | } |
| 201 | |
| 202 | /** |
Kevin Chyn | dba919a | 2018-03-16 14:35:10 -0700 | [diff] [blame] | 203 | * Required: Set the text for the negative button. This would typically be used as a |
| 204 | * "Cancel" button, but may be also used to show an alternative method for authentication, |
| 205 | * such as screen that asks for a backup password. |
Kevin Chyn | 39ebee4 | 2019-01-31 16:24:46 -0800 | [diff] [blame] | 206 | * |
Kevin Chyn | 45d1f9d | 2019-02-07 13:38:04 -0800 | [diff] [blame] | 207 | * Note that this should not be set if {@link #setAllowDeviceCredential(boolean) |
| 208 | * is set to true. |
Kevin Chyn | 39ebee4 | 2019-01-31 16:24:46 -0800 | [diff] [blame] | 209 | * |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 210 | * @param text |
| 211 | * @return |
| 212 | */ |
| 213 | public Builder setNegativeButton(@NonNull CharSequence text, |
| 214 | @NonNull @CallbackExecutor Executor executor, |
| 215 | @NonNull DialogInterface.OnClickListener listener) { |
| 216 | if (TextUtils.isEmpty(text)) { |
| 217 | throw new IllegalArgumentException("Text must be set and non-empty"); |
| 218 | } |
| 219 | if (executor == null) { |
| 220 | throw new IllegalArgumentException("Executor must not be null"); |
| 221 | } |
| 222 | if (listener == null) { |
| 223 | throw new IllegalArgumentException("Listener must not be null"); |
| 224 | } |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 225 | mBundle.putCharSequence(KEY_NEGATIVE_TEXT, text); |
| 226 | mNegativeButtonInfo = new ButtonInfo(executor, listener); |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 227 | return this; |
| 228 | } |
| 229 | |
| 230 | /** |
Kevin Chyn | 158fefb | 2019-01-03 18:59:05 -0800 | [diff] [blame] | 231 | * Optional: A hint to the system to require user confirmation after a biometric has been |
| 232 | * authenticated. For example, implicit modalities like Face and Iris authentication are |
| 233 | * passive, meaning they don't require an explicit user action to complete. When set to |
| 234 | * 'false', the user action (e.g. pressing a button) will not be required. BiometricPrompt |
| 235 | * will require confirmation by default. |
| 236 | * |
| 237 | * A typical use case for not requiring confirmation would be for low-risk transactions, |
| 238 | * such as re-authenticating a recently authenticated application. A typical use case for |
| 239 | * requiring confirmation would be for authorizing a purchase. |
| 240 | * |
| 241 | * Note that this is a hint to the system. The system may choose to ignore the flag. For |
| 242 | * example, if the user disables implicit authentication in Settings, or if it does not |
| 243 | * apply to a modality (e.g. Fingerprint). When ignored, the system will default to |
| 244 | * requiring confirmation. |
| 245 | * |
| 246 | * @param requireConfirmation |
Kevin Chyn | 158fefb | 2019-01-03 18:59:05 -0800 | [diff] [blame] | 247 | */ |
| 248 | public Builder setRequireConfirmation(boolean requireConfirmation) { |
| 249 | mBundle.putBoolean(KEY_REQUIRE_CONFIRMATION, requireConfirmation); |
| 250 | return this; |
| 251 | } |
| 252 | |
| 253 | /** |
Kevin Chyn | 1b2137c | 2019-01-24 16:32:38 -0800 | [diff] [blame] | 254 | * The user will first be prompted to authenticate with biometrics, but also given the |
Kevin Chyn | 45d1f9d | 2019-02-07 13:38:04 -0800 | [diff] [blame] | 255 | * option to authenticate with their device PIN, pattern, or password. Developers should |
| 256 | * first check {@link KeyguardManager#isDeviceSecure()} before enabling this. If the device |
| 257 | * is not secure, {@link BiometricPrompt#BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL} will be |
| 258 | * returned in {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)}} |
Kevin Chyn | 39ebee4 | 2019-01-31 16:24:46 -0800 | [diff] [blame] | 259 | * |
| 260 | * Note that {@link #setNegativeButton(CharSequence, Executor, |
| 261 | * DialogInterface.OnClickListener)} should not be set if this is set to true. |
| 262 | * |
Kevin Chyn | 1b2137c | 2019-01-24 16:32:38 -0800 | [diff] [blame] | 263 | * @param enable When true, the prompt will fall back to ask for the user's device |
| 264 | * credentials (PIN, pattern, or password). |
| 265 | * @return |
| 266 | */ |
Kevin Chyn | 45d1f9d | 2019-02-07 13:38:04 -0800 | [diff] [blame] | 267 | public Builder setAllowDeviceCredential(boolean enable) { |
| 268 | mBundle.putBoolean(KEY_ALLOW_DEVICE_CREDENTIAL, enable); |
Kevin Chyn | 1b2137c | 2019-01-24 16:32:38 -0800 | [diff] [blame] | 269 | return this; |
| 270 | } |
| 271 | |
| 272 | /** |
Vishwath Mohan | ecf00ce | 2018-04-05 10:28:24 -0700 | [diff] [blame] | 273 | * Creates a {@link BiometricPrompt}. |
| 274 | * @return a {@link BiometricPrompt} |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 275 | * @throws IllegalArgumentException if any of the required fields are not set. |
| 276 | */ |
Vishwath Mohan | ecf00ce | 2018-04-05 10:28:24 -0700 | [diff] [blame] | 277 | public BiometricPrompt build() { |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 278 | final CharSequence title = mBundle.getCharSequence(KEY_TITLE); |
| 279 | final CharSequence negative = mBundle.getCharSequence(KEY_NEGATIVE_TEXT); |
Kevin Chyn | 3a018719 | 2018-10-08 15:40:05 -0700 | [diff] [blame] | 280 | final boolean useDefaultTitle = mBundle.getBoolean(KEY_USE_DEFAULT_TITLE); |
Kevin Chyn | 45d1f9d | 2019-02-07 13:38:04 -0800 | [diff] [blame] | 281 | final boolean enableFallback = mBundle.getBoolean(KEY_ALLOW_DEVICE_CREDENTIAL); |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 282 | |
Kevin Chyn | 3a018719 | 2018-10-08 15:40:05 -0700 | [diff] [blame] | 283 | if (TextUtils.isEmpty(title) && !useDefaultTitle) { |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 284 | throw new IllegalArgumentException("Title must be set and non-empty"); |
Kevin Chyn | 1b2137c | 2019-01-24 16:32:38 -0800 | [diff] [blame] | 285 | } else if (TextUtils.isEmpty(negative) && !enableFallback) { |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 286 | throw new IllegalArgumentException("Negative text must be set and non-empty"); |
Kevin Chyn | 1b2137c | 2019-01-24 16:32:38 -0800 | [diff] [blame] | 287 | } else if (!TextUtils.isEmpty(negative) && enableFallback) { |
| 288 | throw new IllegalArgumentException("Can't have both negative button behavior" |
Kevin Chyn | 45d1f9d | 2019-02-07 13:38:04 -0800 | [diff] [blame] | 289 | + " and device credential enabled"); |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 290 | } |
Vishwath Mohan | ecf00ce | 2018-04-05 10:28:24 -0700 | [diff] [blame] | 291 | return new BiometricPrompt(mContext, mBundle, mPositiveButtonInfo, mNegativeButtonInfo); |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 292 | } |
| 293 | } |
| 294 | |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 295 | private class OnAuthenticationCancelListener implements CancellationSignal.OnCancelListener { |
| 296 | @Override |
| 297 | public void onCancel() { |
| 298 | cancelAuthentication(); |
| 299 | } |
| 300 | } |
| 301 | |
| 302 | private final IBinder mToken = new Binder(); |
| 303 | private final Context mContext; |
Kevin Chyn | 352adfe | 2018-09-20 22:28:50 -0700 | [diff] [blame] | 304 | private final IBiometricService mService; |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 305 | private final Bundle mBundle; |
| 306 | private final ButtonInfo mPositiveButtonInfo; |
| 307 | private final ButtonInfo mNegativeButtonInfo; |
| 308 | |
| 309 | private CryptoObject mCryptoObject; |
| 310 | private Executor mExecutor; |
| 311 | private AuthenticationCallback mAuthenticationCallback; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 312 | |
Kevin Chyn | e92cdae | 2018-11-21 16:35:04 -0800 | [diff] [blame] | 313 | private final IBiometricServiceReceiver mBiometricServiceReceiver = |
| 314 | new IBiometricServiceReceiver.Stub() { |
| 315 | |
| 316 | @Override |
| 317 | public void onAuthenticationSucceeded() throws RemoteException { |
| 318 | mExecutor.execute(() -> { |
| 319 | final AuthenticationResult result = new AuthenticationResult(mCryptoObject); |
| 320 | mAuthenticationCallback.onAuthenticationSucceeded(result); |
| 321 | }); |
| 322 | } |
| 323 | |
| 324 | @Override |
Kevin Chyn | e92cdae | 2018-11-21 16:35:04 -0800 | [diff] [blame] | 325 | public void onAuthenticationFailed() throws RemoteException { |
| 326 | mExecutor.execute(() -> { |
| 327 | mAuthenticationCallback.onAuthenticationFailed(); |
| 328 | }); |
| 329 | } |
| 330 | |
| 331 | @Override |
Kevin Chyn | 87f257a | 2018-11-27 16:26:07 -0800 | [diff] [blame] | 332 | public void onError(int error, String message) throws RemoteException { |
Kevin Chyn | e92cdae | 2018-11-21 16:35:04 -0800 | [diff] [blame] | 333 | mExecutor.execute(() -> { |
| 334 | mAuthenticationCallback.onAuthenticationError(error, message); |
| 335 | }); |
| 336 | } |
| 337 | |
| 338 | @Override |
Kevin Chyn | 87f257a | 2018-11-27 16:26:07 -0800 | [diff] [blame] | 339 | public void onAcquired(int acquireInfo, String message) throws RemoteException { |
Kevin Chyn | e92cdae | 2018-11-21 16:35:04 -0800 | [diff] [blame] | 340 | mExecutor.execute(() -> { |
| 341 | mAuthenticationCallback.onAuthenticationHelp(acquireInfo, message); |
| 342 | }); |
| 343 | } |
| 344 | |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 345 | @Override |
Kevin Chyn | 87f257a | 2018-11-27 16:26:07 -0800 | [diff] [blame] | 346 | public void onDialogDismissed(int reason) throws RemoteException { |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 347 | // Check the reason and invoke OnClickListener(s) if necessary |
| 348 | if (reason == DISMISSED_REASON_POSITIVE) { |
| 349 | mPositiveButtonInfo.executor.execute(() -> { |
| 350 | mPositiveButtonInfo.listener.onClick(null, DialogInterface.BUTTON_POSITIVE); |
| 351 | }); |
| 352 | } else if (reason == DISMISSED_REASON_NEGATIVE) { |
| 353 | mNegativeButtonInfo.executor.execute(() -> { |
| 354 | mNegativeButtonInfo.listener.onClick(null, DialogInterface.BUTTON_NEGATIVE); |
| 355 | }); |
| 356 | } |
| 357 | } |
| 358 | }; |
| 359 | |
Vishwath Mohan | ecf00ce | 2018-04-05 10:28:24 -0700 | [diff] [blame] | 360 | private BiometricPrompt(Context context, Bundle bundle, |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 361 | ButtonInfo positiveButtonInfo, ButtonInfo negativeButtonInfo) { |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 362 | mContext = context; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 363 | mBundle = bundle; |
| 364 | mPositiveButtonInfo = positiveButtonInfo; |
| 365 | mNegativeButtonInfo = negativeButtonInfo; |
Kevin Chyn | 352adfe | 2018-09-20 22:28:50 -0700 | [diff] [blame] | 366 | mService = IBiometricService.Stub.asInterface( |
| 367 | ServiceManager.getService(Context.BIOMETRIC_SERVICE)); |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 368 | } |
| 369 | |
| 370 | /** |
Vishwath Mohan | ecf00ce | 2018-04-05 10:28:24 -0700 | [diff] [blame] | 371 | * A wrapper class for the crypto objects supported by BiometricPrompt. Currently the framework |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 372 | * supports {@link Signature}, {@link Cipher} and {@link Mac} objects. |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 373 | */ |
| 374 | public static final class CryptoObject extends android.hardware.biometrics.CryptoObject { |
| 375 | public CryptoObject(@NonNull Signature signature) { |
| 376 | super(signature); |
| 377 | } |
| 378 | |
| 379 | public CryptoObject(@NonNull Cipher cipher) { |
| 380 | super(cipher); |
| 381 | } |
| 382 | |
| 383 | public CryptoObject(@NonNull Mac mac) { |
| 384 | super(mac); |
| 385 | } |
| 386 | |
| 387 | /** |
| 388 | * Get {@link Signature} object. |
| 389 | * @return {@link Signature} object or null if this doesn't contain one. |
| 390 | */ |
| 391 | public Signature getSignature() { |
| 392 | return super.getSignature(); |
| 393 | } |
| 394 | |
| 395 | /** |
| 396 | * Get {@link Cipher} object. |
| 397 | * @return {@link Cipher} object or null if this doesn't contain one. |
| 398 | */ |
| 399 | public Cipher getCipher() { |
| 400 | return super.getCipher(); |
| 401 | } |
| 402 | |
| 403 | /** |
| 404 | * Get {@link Mac} object. |
| 405 | * @return {@link Mac} object or null if this doesn't contain one. |
| 406 | */ |
| 407 | public Mac getMac() { |
| 408 | return super.getMac(); |
| 409 | } |
| 410 | } |
| 411 | |
| 412 | /** |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 413 | * Container for callback data from {@link #authenticate( CancellationSignal, Executor, |
| 414 | * AuthenticationCallback)} and {@link #authenticate(CryptoObject, CancellationSignal, Executor, |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 415 | * AuthenticationCallback)} |
| 416 | */ |
| 417 | public static class AuthenticationResult extends BiometricAuthenticator.AuthenticationResult { |
| 418 | /** |
| 419 | * Authentication result |
| 420 | * @param crypto |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 421 | * @hide |
| 422 | */ |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 423 | public AuthenticationResult(CryptoObject crypto) { |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 424 | // Identifier and userId is not used for BiometricPrompt. |
| 425 | super(crypto, null /* identifier */, 0 /* userId */); |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 426 | } |
| 427 | /** |
| 428 | * Obtain the crypto object associated with this transaction |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 429 | * @return crypto object provided to {@link #authenticate( CryptoObject, CancellationSignal, |
| 430 | * Executor, AuthenticationCallback)} |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 431 | */ |
| 432 | public CryptoObject getCryptoObject() { |
| 433 | return (CryptoObject) super.getCryptoObject(); |
| 434 | } |
| 435 | } |
| 436 | |
| 437 | /** |
Vishwath Mohan | ecf00ce | 2018-04-05 10:28:24 -0700 | [diff] [blame] | 438 | * Callback structure provided to {@link BiometricPrompt#authenticate(CancellationSignal, |
| 439 | * Executor, AuthenticationCallback)} or {@link BiometricPrompt#authenticate(CryptoObject, |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 440 | * CancellationSignal, Executor, AuthenticationCallback)}. Users must provide an implementation |
| 441 | * of this for listening to authentication events. |
| 442 | */ |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 443 | public abstract static class AuthenticationCallback extends |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 444 | BiometricAuthenticator.AuthenticationCallback { |
| 445 | /** |
| 446 | * Called when an unrecoverable error has been encountered and the operation is complete. |
| 447 | * No further actions will be made on this object. |
| 448 | * @param errorCode An integer identifying the error message |
| 449 | * @param errString A human-readable error string that can be shown on an UI |
| 450 | */ |
| 451 | @Override |
| 452 | public void onAuthenticationError(int errorCode, CharSequence errString) {} |
| 453 | |
| 454 | /** |
| 455 | * Called when a recoverable error has been encountered during authentication. The help |
| 456 | * string is provided to give the user guidance for what went wrong, such as "Sensor dirty, |
| 457 | * please clean it." |
| 458 | * @param helpCode An integer identifying the error message |
| 459 | * @param helpString A human-readable string that can be shown on an UI |
| 460 | */ |
| 461 | @Override |
| 462 | public void onAuthenticationHelp(int helpCode, CharSequence helpString) {} |
| 463 | |
| 464 | /** |
| 465 | * Called when a biometric is recognized. |
| 466 | * @param result An object containing authentication-related data |
| 467 | */ |
| 468 | public void onAuthenticationSucceeded(AuthenticationResult result) {} |
| 469 | |
| 470 | /** |
| 471 | * Called when a biometric is valid but not recognized. |
| 472 | */ |
| 473 | @Override |
| 474 | public void onAuthenticationFailed() {} |
| 475 | |
| 476 | /** |
| 477 | * Called when a biometric has been acquired, but hasn't been processed yet. |
| 478 | * @hide |
| 479 | */ |
| 480 | @Override |
| 481 | public void onAuthenticationAcquired(int acquireInfo) {} |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 482 | } |
| 483 | |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 484 | /** |
Kevin Chyn | 067085a | 2018-11-12 15:49:19 -0800 | [diff] [blame] | 485 | * Authenticates for the given user. |
| 486 | * @param cancel An object that can be used to cancel authentication |
| 487 | * @param executor An executor to handle callback events |
| 488 | * @param callback An object to receive authentication events |
| 489 | * @param userId The user to authenticate |
| 490 | * @hide |
| 491 | */ |
| 492 | @RequiresPermission(USE_BIOMETRIC_INTERNAL) |
| 493 | public void authenticateUser(@NonNull CancellationSignal cancel, |
| 494 | @NonNull @CallbackExecutor Executor executor, |
| 495 | @NonNull AuthenticationCallback callback, |
| 496 | int userId) { |
| 497 | if (cancel == null) { |
| 498 | throw new IllegalArgumentException("Must supply a cancellation signal"); |
| 499 | } |
| 500 | if (executor == null) { |
| 501 | throw new IllegalArgumentException("Must supply an executor"); |
| 502 | } |
| 503 | if (callback == null) { |
| 504 | throw new IllegalArgumentException("Must supply a callback"); |
| 505 | } |
| 506 | authenticateInternal(null /* crypto */, cancel, executor, callback, userId); |
| 507 | } |
| 508 | |
| 509 | /** |
Kevin Chyn | 33250a7 | 2018-09-27 16:24:40 -0700 | [diff] [blame] | 510 | * This call warms up the biometric hardware, displays a system-provided dialog, and starts |
| 511 | * scanning for a biometric. It terminates when {@link |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 512 | * AuthenticationCallback#onAuthenticationError(int, CharSequence)} is called, when {@link |
| 513 | * AuthenticationCallback#onAuthenticationSucceeded( AuthenticationResult)}, or when the user |
| 514 | * dismisses the system-provided dialog, at which point the crypto object becomes invalid. This |
| 515 | * operation can be canceled by using the provided cancel object. The application will receive |
| 516 | * authentication errors through {@link AuthenticationCallback}, and button events through the |
| 517 | * corresponding callback set in {@link Builder#setNegativeButton(CharSequence, Executor, |
Vishwath Mohan | ecf00ce | 2018-04-05 10:28:24 -0700 | [diff] [blame] | 518 | * DialogInterface.OnClickListener)}. It is safe to reuse the {@link BiometricPrompt} object, |
| 519 | * and calling {@link BiometricPrompt#authenticate( CancellationSignal, Executor, |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 520 | * AuthenticationCallback)} while an existing authentication attempt is occurring will stop the |
| 521 | * previous client and start a new authentication. The interrupted client will receive a |
| 522 | * cancelled notification through {@link AuthenticationCallback#onAuthenticationError(int, |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 523 | * CharSequence)}. |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 524 | * |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 525 | * @throws IllegalArgumentException If any of the arguments are null |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 526 | * |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 527 | * @param crypto Object associated with the call |
| 528 | * @param cancel An object that can be used to cancel authentication |
| 529 | * @param executor An executor to handle callback events |
| 530 | * @param callback An object to receive authentication events |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 531 | */ |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 532 | @RequiresPermission(USE_BIOMETRIC) |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 533 | public void authenticate(@NonNull CryptoObject crypto, |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 534 | @NonNull CancellationSignal cancel, |
| 535 | @NonNull @CallbackExecutor Executor executor, |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 536 | @NonNull AuthenticationCallback callback) { |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 537 | if (crypto == null) { |
| 538 | throw new IllegalArgumentException("Must supply a crypto object"); |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 539 | } |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 540 | if (cancel == null) { |
| 541 | throw new IllegalArgumentException("Must supply a cancellation signal"); |
| 542 | } |
| 543 | if (executor == null) { |
| 544 | throw new IllegalArgumentException("Must supply an executor"); |
| 545 | } |
| 546 | if (callback == null) { |
| 547 | throw new IllegalArgumentException("Must supply a callback"); |
| 548 | } |
Kevin Chyn | 45d1f9d | 2019-02-07 13:38:04 -0800 | [diff] [blame] | 549 | if (mBundle.getBoolean(KEY_ALLOW_DEVICE_CREDENTIAL)) { |
| 550 | throw new IllegalArgumentException("Device credential not supported with crypto"); |
Kevin Chyn | 1b2137c | 2019-01-24 16:32:38 -0800 | [diff] [blame] | 551 | } |
Kevin Chyn | 067085a | 2018-11-12 15:49:19 -0800 | [diff] [blame] | 552 | authenticateInternal(crypto, cancel, executor, callback, mContext.getUserId()); |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 553 | } |
| 554 | |
| 555 | /** |
Kevin Chyn | 33250a7 | 2018-09-27 16:24:40 -0700 | [diff] [blame] | 556 | * This call warms up the biometric hardware, displays a system-provided dialog, and starts |
| 557 | * scanning for a biometric. It terminates when {@link |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 558 | * AuthenticationCallback#onAuthenticationError(int, CharSequence)} is called, when {@link |
| 559 | * AuthenticationCallback#onAuthenticationSucceeded( AuthenticationResult)} is called, or when |
| 560 | * the user dismisses the system-provided dialog. This operation can be canceled by using the |
| 561 | * provided cancel object. The application will receive authentication errors through {@link |
| 562 | * AuthenticationCallback}, and button events through the corresponding callback set in {@link |
| 563 | * Builder#setNegativeButton(CharSequence, Executor, DialogInterface.OnClickListener)}. It is |
Vishwath Mohan | ecf00ce | 2018-04-05 10:28:24 -0700 | [diff] [blame] | 564 | * safe to reuse the {@link BiometricPrompt} object, and calling {@link |
| 565 | * BiometricPrompt#authenticate(CancellationSignal, Executor, AuthenticationCallback)} while |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 566 | * an existing authentication attempt is occurring will stop the previous client and start a new |
| 567 | * authentication. The interrupted client will receive a cancelled notification through {@link |
| 568 | * AuthenticationCallback#onAuthenticationError(int, CharSequence)}. |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 569 | * |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 570 | * @throws IllegalArgumentException If any of the arguments are null |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 571 | * |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 572 | * @param cancel An object that can be used to cancel authentication |
| 573 | * @param executor An executor to handle callback events |
| 574 | * @param callback An object to receive authentication events |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 575 | */ |
Vishwath Mohan | cf87df1 | 2018-03-20 22:57:17 -0700 | [diff] [blame] | 576 | @RequiresPermission(USE_BIOMETRIC) |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 577 | public void authenticate(@NonNull CancellationSignal cancel, |
| 578 | @NonNull @CallbackExecutor Executor executor, |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 579 | @NonNull AuthenticationCallback callback) { |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 580 | if (cancel == null) { |
| 581 | throw new IllegalArgumentException("Must supply a cancellation signal"); |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 582 | } |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 583 | if (executor == null) { |
| 584 | throw new IllegalArgumentException("Must supply an executor"); |
| 585 | } |
| 586 | if (callback == null) { |
| 587 | throw new IllegalArgumentException("Must supply a callback"); |
| 588 | } |
Kevin Chyn | 067085a | 2018-11-12 15:49:19 -0800 | [diff] [blame] | 589 | authenticateInternal(null /* crypto */, cancel, executor, callback, mContext.getUserId()); |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 590 | } |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 591 | |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 592 | private void cancelAuthentication() { |
| 593 | if (mService != null) { |
| 594 | try { |
| 595 | mService.cancelAuthentication(mToken, mContext.getOpPackageName()); |
| 596 | } catch (RemoteException e) { |
| 597 | Log.e(TAG, "Unable to cancel authentication", e); |
| 598 | } |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 599 | } |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 600 | } |
| 601 | |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 602 | private void authenticateInternal(@Nullable CryptoObject crypto, |
| 603 | @NonNull CancellationSignal cancel, |
| 604 | @NonNull @CallbackExecutor Executor executor, |
Kevin Chyn | 067085a | 2018-11-12 15:49:19 -0800 | [diff] [blame] | 605 | @NonNull AuthenticationCallback callback, |
| 606 | int userId) { |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 607 | try { |
| 608 | if (cancel.isCanceled()) { |
| 609 | Log.w(TAG, "Authentication already canceled"); |
| 610 | return; |
| 611 | } else { |
| 612 | cancel.setOnCancelListener(new OnAuthenticationCancelListener()); |
| 613 | } |
| 614 | |
| 615 | mCryptoObject = crypto; |
| 616 | mExecutor = executor; |
| 617 | mAuthenticationCallback = callback; |
| 618 | final long sessionId = crypto != null ? crypto.getOpId() : 0; |
Kevin Chyn | e92cdae | 2018-11-21 16:35:04 -0800 | [diff] [blame] | 619 | mService.authenticate(mToken, sessionId, userId, mBiometricServiceReceiver, |
Kevin Chyn | 87f257a | 2018-11-27 16:26:07 -0800 | [diff] [blame] | 620 | mContext.getOpPackageName(), mBundle); |
Kevin Chyn | a24e9fd | 2018-08-27 12:39:17 -0700 | [diff] [blame] | 621 | } catch (RemoteException e) { |
| 622 | Log.e(TAG, "Remote exception while authenticating", e); |
| 623 | mExecutor.execute(() -> { |
| 624 | callback.onAuthenticationError(BiometricPrompt.BIOMETRIC_ERROR_HW_UNAVAILABLE, |
| 625 | mContext.getString(R.string.biometric_error_hw_unavailable)); |
| 626 | }); |
| 627 | } |
Kevin Chyn | 6668256 | 2018-01-25 18:26:46 -0800 | [diff] [blame] | 628 | } |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 629 | } |