1 /* 2 * Copyright (C) 2020 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 com.android.settings.biometrics; 18 19 import android.annotation.IntDef; 20 import android.app.Activity; 21 import android.app.PendingIntent; 22 import android.app.admin.DevicePolicyManager; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentSender; 26 import android.hardware.biometrics.SensorProperties; 27 import android.hardware.face.FaceManager; 28 import android.hardware.face.FaceSensorPropertiesInternal; 29 import android.os.storage.StorageManager; 30 import android.util.Log; 31 import android.view.Surface; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 import androidx.fragment.app.FragmentActivity; 36 37 import com.android.internal.widget.LockPatternUtils; 38 import com.android.internal.widget.VerifyCredentialResponse; 39 import com.android.settings.SetupWizardUtils; 40 import com.android.settings.biometrics.face.FaceEnrollIntroduction; 41 import com.android.settings.biometrics.fingerprint.FingerprintEnrollFindSensor; 42 import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroduction; 43 import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollFindSensor; 44 import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction; 45 import com.android.settings.password.ChooseLockGeneric; 46 import com.android.settings.password.ChooseLockSettingsHelper; 47 import com.android.settings.password.SetupChooseLockGeneric; 48 49 import com.google.android.setupcompat.util.WizardManagerHelper; 50 51 import java.lang.annotation.Retention; 52 import java.lang.annotation.RetentionPolicy; 53 54 /** 55 * Common biometric utilities. 56 */ 57 public class BiometricUtils { 58 private static final String TAG = "BiometricUtils"; 59 60 // Note: Theis IntDef must align SystemUI DevicePostureInt 61 @IntDef(prefix = {"DEVICE_POSTURE_"}, value = { 62 DEVICE_POSTURE_UNKNOWN, 63 DEVICE_POSTURE_CLOSED, 64 DEVICE_POSTURE_HALF_OPENED, 65 DEVICE_POSTURE_OPENED, 66 DEVICE_POSTURE_FLIPPED 67 }) 68 @Retention(RetentionPolicy.SOURCE) 69 public @interface DevicePostureInt {} 70 71 // NOTE: These constants **must** match those defined for Jetpack Sidecar. This is because we 72 // use the Device State -> Jetpack Posture map in DevicePostureControllerImpl to translate 73 // between the two. 74 public static final int DEVICE_POSTURE_UNKNOWN = 0; 75 public static final int DEVICE_POSTURE_CLOSED = 1; 76 public static final int DEVICE_POSTURE_HALF_OPENED = 2; 77 public static final int DEVICE_POSTURE_OPENED = 3; 78 public static final int DEVICE_POSTURE_FLIPPED = 4; 79 80 public static int sAllowEnrollPosture = DEVICE_POSTURE_UNKNOWN; 81 82 /** 83 * Request was sent for starting another enrollment of a previously 84 * enrolled biometric of the same type. 85 */ 86 public static int REQUEST_ADD_ANOTHER = 7; 87 88 /** 89 * Gatekeeper credential not match exception, it throws if VerifyCredentialResponse is not 90 * matched in requestGatekeeperHat(). 91 */ 92 public static class GatekeeperCredentialNotMatchException extends IllegalStateException { GatekeeperCredentialNotMatchException(String s)93 public GatekeeperCredentialNotMatchException(String s) { 94 super(s); 95 } 96 }; 97 98 /** 99 * Given the result from confirming or choosing a credential, request Gatekeeper to generate 100 * a HardwareAuthToken with the Gatekeeper Password together with a biometric challenge. 101 * 102 * @param context Caller's context 103 * @param result The onActivityResult intent from ChooseLock* or ConfirmLock* 104 * @param userId User ID that the credential/biometric operation applies to 105 * @param challenge Unique biometric challenge from FingerprintManager/FaceManager 106 * @return 107 * @throws GatekeeperCredentialNotMatchException if Gatekeeper response is not match 108 * @throws IllegalStateException if Gatekeeper Password is missing 109 */ requestGatekeeperHat(@onNull Context context, @NonNull Intent result, int userId, long challenge)110 public static byte[] requestGatekeeperHat(@NonNull Context context, @NonNull Intent result, 111 int userId, long challenge) { 112 if (!containsGatekeeperPasswordHandle(result)) { 113 throw new IllegalStateException("Gatekeeper Password is missing!!"); 114 } 115 final long gatekeeperPasswordHandle = result.getLongExtra( 116 ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 0L); 117 return requestGatekeeperHat(context, gatekeeperPasswordHandle, userId, challenge); 118 } 119 requestGatekeeperHat(@onNull Context context, long gkPwHandle, int userId, long challenge)120 public static byte[] requestGatekeeperHat(@NonNull Context context, long gkPwHandle, int userId, 121 long challenge) { 122 final LockPatternUtils utils = new LockPatternUtils(context); 123 final VerifyCredentialResponse response = utils.verifyGatekeeperPasswordHandle(gkPwHandle, 124 challenge, userId); 125 if (!response.isMatched()) { 126 throw new GatekeeperCredentialNotMatchException("Unable to request Gatekeeper HAT"); 127 } 128 return response.getGatekeeperHAT(); 129 } 130 containsGatekeeperPasswordHandle(@ullable Intent data)131 public static boolean containsGatekeeperPasswordHandle(@Nullable Intent data) { 132 return data != null && data.hasExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE); 133 } 134 getGatekeeperPasswordHandle(@onNull Intent data)135 public static long getGatekeeperPasswordHandle(@NonNull Intent data) { 136 return data.getLongExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 0L); 137 } 138 139 /** 140 * Requests {@link com.android.server.locksettings.LockSettingsService} to remove the 141 * gatekeeper password associated with a previous 142 * {@link ChooseLockSettingsHelper.Builder#setRequestGatekeeperPasswordHandle(boolean)} 143 * 144 * @param context Caller's context 145 * @param data The onActivityResult intent from ChooseLock* or ConfirmLock* 146 */ removeGatekeeperPasswordHandle(@onNull Context context, @Nullable Intent data)147 public static void removeGatekeeperPasswordHandle(@NonNull Context context, 148 @Nullable Intent data) { 149 if (data == null) { 150 return; 151 } 152 if (!containsGatekeeperPasswordHandle(data)) { 153 return; 154 } 155 removeGatekeeperPasswordHandle(context, getGatekeeperPasswordHandle(data)); 156 } 157 removeGatekeeperPasswordHandle(@onNull Context context, long handle)158 public static void removeGatekeeperPasswordHandle(@NonNull Context context, long handle) { 159 final LockPatternUtils utils = new LockPatternUtils(context); 160 utils.removeGatekeeperPasswordHandle(handle); 161 Log.d(TAG, "Removed handle"); 162 } 163 164 /** 165 * @param context caller's context 166 * @param activityIntent The intent that started the caller's activity 167 * @return Intent for starting ChooseLock* 168 */ getChooseLockIntent(@onNull Context context, @NonNull Intent activityIntent)169 public static Intent getChooseLockIntent(@NonNull Context context, 170 @NonNull Intent activityIntent) { 171 if (WizardManagerHelper.isAnySetupWizard(activityIntent)) { 172 // Default to PIN lock in setup wizard 173 Intent intent = new Intent(context, SetupChooseLockGeneric.class); 174 if (StorageManager.isFileEncryptedNativeOrEmulated()) { 175 intent.putExtra( 176 LockPatternUtils.PASSWORD_TYPE_KEY, 177 DevicePolicyManager.PASSWORD_QUALITY_NUMERIC); 178 intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment 179 .EXTRA_SHOW_OPTIONS_BUTTON, true); 180 } 181 WizardManagerHelper.copyWizardManagerExtras(activityIntent, intent); 182 return intent; 183 } else { 184 return new Intent(context, ChooseLockGeneric.class); 185 } 186 } 187 188 /** 189 * @param context caller's context 190 * @param activityIntent The intent that started the caller's activity 191 * @return Intent for starting FingerprintEnrollFindSensor 192 */ getFingerprintFindSensorIntent(@onNull Context context, @NonNull Intent activityIntent)193 public static Intent getFingerprintFindSensorIntent(@NonNull Context context, 194 @NonNull Intent activityIntent) { 195 if (WizardManagerHelper.isAnySetupWizard(activityIntent)) { 196 Intent intent = new Intent(context, SetupFingerprintEnrollFindSensor.class); 197 SetupWizardUtils.copySetupExtras(activityIntent, intent); 198 return intent; 199 } else { 200 return new Intent(context, FingerprintEnrollFindSensor.class); 201 } 202 } 203 204 /** 205 * @param context caller's context 206 * @param activityIntent The intent that started the caller's activity 207 * @return Intent for starting FingerprintEnrollIntroduction 208 */ getFingerprintIntroIntent(@onNull Context context, @NonNull Intent activityIntent)209 public static Intent getFingerprintIntroIntent(@NonNull Context context, 210 @NonNull Intent activityIntent) { 211 if (WizardManagerHelper.isAnySetupWizard(activityIntent)) { 212 Intent intent = new Intent(context, SetupFingerprintEnrollIntroduction.class); 213 WizardManagerHelper.copyWizardManagerExtras(activityIntent, intent); 214 return intent; 215 } else { 216 return new Intent(context, FingerprintEnrollIntroduction.class); 217 } 218 } 219 220 /** 221 * @param context caller's context 222 * @param activityIntent The intent that started the caller's activity 223 * @return Intent for starting FaceEnrollIntroduction 224 */ getFaceIntroIntent(@onNull Context context, @NonNull Intent activityIntent)225 public static Intent getFaceIntroIntent(@NonNull Context context, 226 @NonNull Intent activityIntent) { 227 final Intent intent = new Intent(context, FaceEnrollIntroduction.class); 228 WizardManagerHelper.copyWizardManagerExtras(activityIntent, intent); 229 return intent; 230 } 231 232 /** 233 * Start an activity that prompts the user to hand the device to their parent or guardian. 234 * @param context caller's context 235 * @param activityIntent The intent that started the caller's activity 236 * @return Intent for starting BiometricHandoffActivity 237 */ getHandoffToParentIntent(@onNull Context context, @NonNull Intent activityIntent)238 public static Intent getHandoffToParentIntent(@NonNull Context context, 239 @NonNull Intent activityIntent) { 240 final Intent intent = new Intent(context, BiometricHandoffActivity.class); 241 WizardManagerHelper.copyWizardManagerExtras(activityIntent, intent); 242 return intent; 243 } 244 245 /** 246 * @param activity Reference to the calling activity, used to startActivity 247 * @param intent Intent pointing to the enrollment activity 248 * @param requestCode If non-zero, will invoke startActivityForResult instead of startActivity 249 * @param hardwareAuthToken HardwareAuthToken from Gatekeeper 250 * @param userId User to request enrollment for 251 */ launchEnrollForResult(@onNull FragmentActivity activity, @NonNull Intent intent, int requestCode, @Nullable byte[] hardwareAuthToken, @Nullable Long gkPwHandle, int userId)252 public static void launchEnrollForResult(@NonNull FragmentActivity activity, 253 @NonNull Intent intent, int requestCode, 254 @Nullable byte[] hardwareAuthToken, @Nullable Long gkPwHandle, int userId) { 255 if (hardwareAuthToken != null) { 256 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, 257 hardwareAuthToken); 258 } 259 if (gkPwHandle != null) { 260 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, (long) gkPwHandle); 261 } 262 263 if (activity instanceof BiometricEnrollActivity.InternalActivity) { 264 intent.putExtra(Intent.EXTRA_USER_ID, userId); 265 } 266 267 if (requestCode != 0) { 268 activity.startActivityForResult(intent, requestCode); 269 } else { 270 activity.startActivity(intent); 271 activity.finish(); 272 } 273 } 274 275 /** 276 * Used for checking if a multi-biometric enrollment flow starts with Face and 277 * ends with Fingerprint. 278 * 279 * @param activity Activity that we want to check 280 * @return True if the activity is going through a multi-biometric enrollment flow, that starts 281 * with Face. 282 */ isMultiBiometricFaceEnrollmentFlow(@onNull Activity activity)283 public static boolean isMultiBiometricFaceEnrollmentFlow(@NonNull Activity activity) { 284 return activity.getIntent().hasExtra(MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FACE); 285 } 286 287 /** 288 * Used for checking if a multi-biometric enrollment flowstarts with Fingerprint 289 * and ends with Face. 290 * 291 * @param activity Activity that we want to check 292 * @return True if the activity is going through a multi-biometric enrollment flow, that starts 293 * with Fingerprint. 294 */ isMultiBiometricFingerprintEnrollmentFlow(@onNull Activity activity)295 public static boolean isMultiBiometricFingerprintEnrollmentFlow(@NonNull Activity activity) { 296 return activity.getIntent().hasExtra( 297 MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FINGERPRINT); 298 } 299 300 /** 301 * Used to check if the activity is a multi biometric flow activity. 302 * 303 * @param activity Activity to check 304 * @return True if the activity is going through a multi-biometric enrollment flow, that starts 305 * with Fingerprint. 306 */ isAnyMultiBiometricFlow(@onNull Activity activity)307 public static boolean isAnyMultiBiometricFlow(@NonNull Activity activity) { 308 return isMultiBiometricFaceEnrollmentFlow(activity) 309 || isMultiBiometricFingerprintEnrollmentFlow(activity); 310 } 311 312 /** 313 * Used to check if the activity is showing a posture guidance to user. 314 * 315 * @param devicePosture the device posture state 316 * @param isLaunchedPostureGuidance True launching a posture guidance to user 317 * @return True if the activity is showing posture guidance to user 318 */ isPostureGuidanceShowing(@evicePostureInt int devicePosture, boolean isLaunchedPostureGuidance)319 public static boolean isPostureGuidanceShowing(@DevicePostureInt int devicePosture, 320 boolean isLaunchedPostureGuidance) { 321 return !isPostureAllowEnrollment(devicePosture) && isLaunchedPostureGuidance; 322 } 323 324 /** 325 * Used to check if current device posture state is allow to enroll biometrics. 326 * For compatibility, we don't restrict enrollment if device do not config. 327 * 328 * @param devicePosture True if current device posture allow enrollment 329 * @return True if current device posture state allow enrollment 330 */ isPostureAllowEnrollment(@evicePostureInt int devicePosture)331 public static boolean isPostureAllowEnrollment(@DevicePostureInt int devicePosture) { 332 return (sAllowEnrollPosture == DEVICE_POSTURE_UNKNOWN) 333 || (devicePosture == sAllowEnrollPosture); 334 } 335 336 /** 337 * Used to check if the activity should show a posture guidance to user. 338 * 339 * @param devicePosture the device posture state 340 * @param isLaunchedPostureGuidance True launching a posture guidance to user 341 * @return True if posture disallow enroll and posture guidance not showing, false otherwise. 342 */ shouldShowPostureGuidance(@evicePostureInt int devicePosture, boolean isLaunchedPostureGuidance)343 public static boolean shouldShowPostureGuidance(@DevicePostureInt int devicePosture, 344 boolean isLaunchedPostureGuidance) { 345 return !isPostureAllowEnrollment(devicePosture) && !isLaunchedPostureGuidance; 346 } 347 348 /** 349 * Sets allowed device posture for face enrollment. 350 * 351 * @param devicePosture the allowed posture state {@link DevicePostureInt} for enrollment 352 */ setDevicePosturesAllowEnroll(@evicePostureInt int devicePosture)353 public static void setDevicePosturesAllowEnroll(@DevicePostureInt int devicePosture) { 354 sAllowEnrollPosture = devicePosture; 355 } 356 copyMultiBiometricExtras(@onNull Intent fromIntent, @NonNull Intent toIntent)357 public static void copyMultiBiometricExtras(@NonNull Intent fromIntent, 358 @NonNull Intent toIntent) { 359 PendingIntent pendingIntent = (PendingIntent) fromIntent.getExtra( 360 MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FACE, null); 361 if (pendingIntent != null) { 362 toIntent.putExtra(MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FACE, 363 pendingIntent); 364 } 365 366 pendingIntent = (PendingIntent) fromIntent.getExtra( 367 MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FINGERPRINT, null); 368 if (pendingIntent != null) { 369 toIntent.putExtra(MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FINGERPRINT, 370 pendingIntent); 371 } 372 } 373 374 /** 375 * If the current biometric enrollment (e.g. face/fingerprint) should be followed by another 376 * one (e.g. fingerprint/face) retrieves the PendingIntent pointing to the next enrollment 377 * and starts it. The caller will receive the result in onActivityResult. 378 * @return true if the next enrollment was started 379 */ tryStartingNextBiometricEnroll(@onNull Activity activity, int requestCode, String debugReason)380 public static boolean tryStartingNextBiometricEnroll(@NonNull Activity activity, 381 int requestCode, String debugReason) { 382 383 PendingIntent pendingIntent = (PendingIntent) activity.getIntent() 384 .getExtra(MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FACE); 385 if (pendingIntent == null) { 386 pendingIntent = (PendingIntent) activity.getIntent() 387 .getExtra(MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FINGERPRINT); 388 } 389 390 if (pendingIntent != null) { 391 try { 392 IntentSender intentSender = pendingIntent.getIntentSender(); 393 activity.startIntentSenderForResult(intentSender, requestCode, 394 null /* fillInIntent */, 0 /* flagMask */, 0 /* flagValues */, 395 0 /* extraFlags */); 396 return true; 397 } catch (IntentSender.SendIntentException e) { 398 Log.e(TAG, "Pending intent canceled: " + e); 399 } 400 } 401 return false; 402 } 403 404 /** 405 * Returns {@code true} if the screen is going into a landscape mode and the angle is equal to 406 * 270. 407 * @param context Context that we use to get the display this context is associated with 408 * @return True if the angle of the rotation is equal to 270. 409 */ isReverseLandscape(@onNull Context context)410 public static boolean isReverseLandscape(@NonNull Context context) { 411 return context.getDisplay().getRotation() == Surface.ROTATION_270; 412 } 413 414 /** 415 * @param faceManager 416 * @return True if at least one sensor is set as a convenience. 417 */ isConvenience(@onNull FaceManager faceManager)418 public static boolean isConvenience(@NonNull FaceManager faceManager) { 419 for (FaceSensorPropertiesInternal props : faceManager.getSensorPropertiesInternal()) { 420 if (props.sensorStrength == SensorProperties.STRENGTH_CONVENIENCE) { 421 return true; 422 } 423 } 424 return false; 425 } 426 427 /** 428 * Returns {@code true} if the screen is going into a landscape mode and the angle is equal to 429 * 90. 430 * @param context Context that we use to get the display this context is associated with 431 * @return True if the angle of the rotation is equal to 90. 432 */ isLandscape(@onNull Context context)433 public static boolean isLandscape(@NonNull Context context) { 434 return context.getDisplay().getRotation() == Surface.ROTATION_90; 435 } 436 } 437