• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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