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