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