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