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