• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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.fingerprint;
18 
19 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_USER_CANCELED;
20 import static android.text.Layout.HYPHENATION_FREQUENCY_NONE;
21 
22 import android.animation.Animator;
23 import android.animation.ObjectAnimator;
24 import android.annotation.IntDef;
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.annotation.RawRes;
28 import android.app.Dialog;
29 import android.app.settings.SettingsEnums;
30 import android.content.Context;
31 import android.content.DialogInterface;
32 import android.content.Intent;
33 import android.content.res.ColorStateList;
34 import android.content.res.Configuration;
35 import android.content.res.Resources;
36 import android.graphics.PorterDuff;
37 import android.graphics.PorterDuffColorFilter;
38 import android.graphics.drawable.Animatable2;
39 import android.graphics.drawable.AnimatedVectorDrawable;
40 import android.graphics.drawable.Drawable;
41 import android.graphics.drawable.LayerDrawable;
42 import android.hardware.fingerprint.FingerprintManager;
43 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
44 import android.os.Bundle;
45 import android.os.Process;
46 import android.os.VibrationAttributes;
47 import android.os.VibrationEffect;
48 import android.os.Vibrator;
49 import android.text.TextUtils;
50 import android.util.Log;
51 import android.view.MotionEvent;
52 import android.view.OrientationEventListener;
53 import android.view.Surface;
54 import android.view.View;
55 import android.view.accessibility.AccessibilityEvent;
56 import android.view.accessibility.AccessibilityManager;
57 import android.view.animation.AccelerateDecelerateInterpolator;
58 import android.view.animation.AnimationUtils;
59 import android.view.animation.Interpolator;
60 import android.widget.LinearLayout;
61 import android.widget.ProgressBar;
62 import android.widget.RelativeLayout;
63 import android.widget.TextView;
64 
65 import androidx.annotation.IdRes;
66 import androidx.appcompat.app.AlertDialog;
67 
68 import com.android.internal.annotations.VisibleForTesting;
69 import com.android.settings.R;
70 import com.android.settings.biometrics.BiometricEnrollSidecar;
71 import com.android.settings.biometrics.BiometricUtils;
72 import com.android.settings.biometrics.BiometricsEnrollEnrolling;
73 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
74 import com.android.settingslib.display.DisplayDensityUtils;
75 
76 import com.airbnb.lottie.LottieAnimationView;
77 import com.airbnb.lottie.LottieCompositionFactory;
78 import com.airbnb.lottie.LottieProperty;
79 import com.airbnb.lottie.model.KeyPath;
80 import com.google.android.setupcompat.template.FooterBarMixin;
81 import com.google.android.setupcompat.template.FooterButton;
82 import com.google.android.setupcompat.util.WizardManagerHelper;
83 import com.google.android.setupdesign.GlifLayout;
84 import com.google.android.setupdesign.template.DescriptionMixin;
85 import com.google.android.setupdesign.template.HeaderMixin;
86 
87 import java.lang.annotation.Retention;
88 import java.lang.annotation.RetentionPolicy;
89 import java.util.List;
90 import java.util.Locale;
91 
92 /**
93  * Activity which handles the actual enrolling for fingerprint.
94  */
95 public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
96 
97     private static final String TAG = "FingerprintEnrollEnrolling";
98     static final String TAG_SIDECAR = "sidecar";
99     static final String KEY_STATE_CANCELED = "is_canceled";
100     static final String KEY_STATE_PREVIOUS_ROTATION = "previous_rotation";
101 
102     private static final int PROGRESS_BAR_MAX = 10000;
103 
104     private static final int STAGE_UNKNOWN = -1;
105     private static final int STAGE_CENTER = 0;
106     private static final int STAGE_GUIDED = 1;
107     private static final int STAGE_FINGERTIP = 2;
108     private static final int STAGE_LEFT_EDGE = 3;
109     private static final int STAGE_RIGHT_EDGE = 4;
110 
111     @VisibleForTesting
112     protected static final int SFPS_STAGE_NO_ANIMATION = 0;
113 
114     @VisibleForTesting
115     protected static final int SFPS_STAGE_CENTER = 1;
116 
117     @VisibleForTesting
118     protected static final int SFPS_STAGE_FINGERTIP = 2;
119 
120     @VisibleForTesting
121     protected static final int SFPS_STAGE_LEFT_EDGE = 3;
122 
123     @VisibleForTesting
124     protected static final int SFPS_STAGE_RIGHT_EDGE = 4;
125 
126     @IntDef({STAGE_UNKNOWN, STAGE_CENTER, STAGE_GUIDED, STAGE_FINGERTIP, STAGE_LEFT_EDGE,
127             STAGE_RIGHT_EDGE})
128     @Retention(RetentionPolicy.SOURCE)
129     private @interface EnrollStage {}
130 
131 
132     @VisibleForTesting
133     @IntDef({STAGE_UNKNOWN, SFPS_STAGE_NO_ANIMATION, SFPS_STAGE_CENTER, SFPS_STAGE_FINGERTIP,
134             SFPS_STAGE_LEFT_EDGE, SFPS_STAGE_RIGHT_EDGE})
135     @Retention(RetentionPolicy.SOURCE)
136     protected @interface SfpsEnrollStage {}
137 
138     /**
139      * If we don't see progress during this time, we show an error message to remind the users that
140      * they need to lift the finger and touch again.
141      */
142     private static final int HINT_TIMEOUT_DURATION = 2500;
143 
144     /**
145      * How long the user needs to touch the icon until we show the dialog.
146      */
147     private static final long ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN = 500;
148 
149     /**
150      * How many times the user needs to touch the icon until we show the dialog that this is not the
151      * fingerprint sensor.
152      */
153     private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3;
154 
155     private static final VibrationEffect VIBRATE_EFFECT_ERROR =
156             VibrationEffect.createWaveform(new long[] {0, 5, 55, 60}, -1);
157     private static final VibrationAttributes FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES =
158             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_ACCESSIBILITY);
159 
160     private FingerprintManager mFingerprintManager;
161     private boolean mCanAssumeUdfps;
162     private boolean mCanAssumeSfps;
163     @Nullable private ProgressBar mProgressBar;
164     private ObjectAnimator mProgressAnim;
165     private TextView mErrorText;
166     private Interpolator mFastOutSlowInInterpolator;
167     private Interpolator mLinearOutSlowInInterpolator;
168     private Interpolator mFastOutLinearInInterpolator;
169     private int mIconTouchCount;
170     private boolean mAnimationCancelled;
171     @Nullable private AnimatedVectorDrawable mIconAnimationDrawable;
172     @Nullable private AnimatedVectorDrawable mIconBackgroundBlinksDrawable;
173     private boolean mRestoring;
174     private Vibrator mVibrator;
175     private boolean mIsSetupWizard;
176     private boolean mIsOrientationChanged;
177     @VisibleForTesting
178     boolean mIsCanceled;
179     private AccessibilityManager mAccessibilityManager;
180     private boolean mIsAccessibilityEnabled;
181     private LottieAnimationView mIllustrationLottie;
182     private boolean mHaveShownUdfpsTipLottie;
183     private boolean mHaveShownUdfpsLeftEdgeLottie;
184     private boolean mHaveShownUdfpsRightEdgeLottie;
185     private boolean mHaveShownSfpsNoAnimationLottie;
186     private boolean mHaveShownSfpsCenterLottie;
187     private boolean mHaveShownSfpsTipLottie;
188     private boolean mHaveShownSfpsLeftEdgeLottie;
189     private boolean mHaveShownSfpsRightEdgeLottie;
190     private boolean mShouldShowLottie;
191     private ObjectAnimator mHelpAnimation;
192 
193     private OrientationEventListener mOrientationEventListener;
194     private int mPreviousRotation = 0;
195 
196     @VisibleForTesting
shouldShowLottie()197     protected boolean shouldShowLottie() {
198         DisplayDensityUtils displayDensity = new DisplayDensityUtils(getApplicationContext());
199         int currentDensityIndex = displayDensity.getCurrentIndexForDefaultDisplay();
200         final int currentDensity = displayDensity.getDefaultDisplayDensityValues()
201                 [currentDensityIndex];
202         final int defaultDensity = displayDensity.getDefaultDensityForDefaultDisplay();
203         return defaultDensity == currentDensity;
204     }
205 
206     @Override
onWindowFocusChanged(boolean hasFocus)207     public void onWindowFocusChanged(boolean hasFocus) {
208         if (hasFocus || mIsCanceled) {
209             return;
210         }
211 
212         // By UX design, we should ensure seamless enrollment CUJ even though user rotate device.
213         // Do NOT cancel enrollment progress after rotating, adding mIsOrientationChanged
214         // to judge if the focus changed was triggered by rotation, current WMS has triple callbacks
215         // (true > false > true), we need to reset mIsOrientationChanged when !hasFocus callback.
216         // Side fps do not have to synchronize udfpsController overlay state, we should bypass sfps
217         // from onWindowFocusChanged() as long press sfps power key will prompt dialog to users.
218         if (!mIsOrientationChanged && !mCanAssumeSfps) {
219             onCancelEnrollment(FINGERPRINT_ERROR_USER_CANCELED);
220         } else {
221             mIsOrientationChanged = false;
222         }
223     }
224 
225     @Override
onApplyThemeResource(Resources.Theme theme, int resid, boolean first)226     protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {
227         theme.applyStyle(R.style.SetupWizardPartnerResource, true);
228         super.onApplyThemeResource(theme, resid, first);
229     }
230 
231     @Override
onCreate(Bundle savedInstanceState)232     protected void onCreate(Bundle savedInstanceState) {
233         super.onCreate(savedInstanceState);
234 
235         if (savedInstanceState != null) {
236             restoreSavedState(savedInstanceState);
237         }
238         mFingerprintManager = getSystemService(FingerprintManager.class);
239         final List<FingerprintSensorPropertiesInternal> props =
240                 mFingerprintManager.getSensorPropertiesInternal();
241         mCanAssumeUdfps = props != null && props.size() == 1 && props.get(0).isAnyUdfpsType();
242         mCanAssumeSfps = props != null && props.size() == 1 && props.get(0).isAnySidefpsType();
243 
244         mAccessibilityManager = getSystemService(AccessibilityManager.class);
245         mIsAccessibilityEnabled = mAccessibilityManager.isEnabled();
246 
247         final boolean isLayoutRtl = (TextUtils.getLayoutDirectionFromLocale(
248                 Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL);
249         listenOrientationEvent();
250 
251         if (mCanAssumeUdfps) {
252             switch(getApplicationContext().getDisplay().getRotation()) {
253                 case Surface.ROTATION_90:
254                     final GlifLayout layout = (GlifLayout) getLayoutInflater().inflate(
255                             R.layout.udfps_enroll_enrolling, null, false);
256                     final LinearLayout layoutContainer = layout.findViewById(
257                             R.id.layout_container);
258                     final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
259                             LinearLayout.LayoutParams.MATCH_PARENT,
260                             LinearLayout.LayoutParams.MATCH_PARENT);
261 
262                     lp.setMarginEnd((int) getResources().getDimension(
263                             R.dimen.rotation_90_enroll_margin_end));
264                     layoutContainer.setPaddingRelative((int) getResources().getDimension(
265                             R.dimen.rotation_90_enroll_padding_start), 0, isLayoutRtl
266                             ? 0 : (int) getResources().getDimension(
267                                     R.dimen.rotation_90_enroll_padding_end), 0);
268                     layoutContainer.setLayoutParams(lp);
269                     setContentView(layout, lp);
270                     break;
271 
272                 case Surface.ROTATION_0:
273                 case Surface.ROTATION_180:
274                 case Surface.ROTATION_270:
275                 default:
276                     setContentView(R.layout.udfps_enroll_enrolling);
277                     break;
278             }
279             setDescriptionText(R.string.security_settings_udfps_enroll_start_message);
280         } else if (mCanAssumeSfps) {
281             setContentView(R.layout.sfps_enroll_enrolling);
282             setHelpAnimation();
283         } else {
284             setContentView(R.layout.fingerprint_enroll_enrolling);
285             setDescriptionText(R.string.security_settings_fingerprint_enroll_start_message);
286         }
287 
288         mIsSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent());
289         if (mCanAssumeUdfps || mCanAssumeSfps) {
290             updateTitleAndDescription();
291         } else {
292             setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title);
293         }
294 
295         mShouldShowLottie = shouldShowLottie();
296         // On non-SFPS devices, only show the lottie if the current display density is the default
297         // density. Otherwise, the lottie will overlap with the settings header text.
298         boolean isLandscape = BiometricUtils.isReverseLandscape(getApplicationContext())
299                 || BiometricUtils.isLandscape(getApplicationContext());
300 
301         updateOrientation((isLandscape
302                 ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT));
303 
304         mErrorText = findViewById(R.id.error_text);
305         mProgressBar = findViewById(R.id.fingerprint_progress_bar);
306         mVibrator = getSystemService(Vibrator.class);
307 
308         mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class);
309         mFooterBarMixin.setSecondaryButton(
310                 new FooterButton.Builder(this)
311                         .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
312                         .setListener(this::onSkipButtonClick)
313                         .setButtonType(FooterButton.ButtonType.SKIP)
314                         .setTheme(R.style.SudGlifButton_Secondary)
315                         .build()
316         );
317 
318         final LayerDrawable fingerprintDrawable = mProgressBar != null
319                 ? (LayerDrawable) mProgressBar.getBackground() : null;
320         if (fingerprintDrawable != null) {
321             mIconAnimationDrawable = (AnimatedVectorDrawable)
322                     fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_animation);
323             mIconBackgroundBlinksDrawable = (AnimatedVectorDrawable)
324                     fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_background);
325             mIconAnimationDrawable.registerAnimationCallback(mIconAnimationCallback);
326         }
327 
328         mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
329                 this, android.R.interpolator.fast_out_slow_in);
330         mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
331                 this, android.R.interpolator.linear_out_slow_in);
332         mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
333                 this, android.R.interpolator.fast_out_linear_in);
334         if (mProgressBar != null) {
335             mProgressBar.setProgressBackgroundTintMode(PorterDuff.Mode.SRC);
336             mProgressBar.setOnTouchListener((v, event) -> {
337                 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
338                     mIconTouchCount++;
339                     if (mIconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) {
340                         showIconTouchDialog();
341                     } else {
342                         mProgressBar.postDelayed(mShowDialogRunnable,
343                                 ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN);
344                     }
345                 } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
346                         || event.getActionMasked() == MotionEvent.ACTION_UP) {
347                     mProgressBar.removeCallbacks(mShowDialogRunnable);
348                 }
349                 return true;
350             });
351         }
352 
353         final Configuration config = getApplicationContext().getResources().getConfiguration();
354         maybeHideSfpsText(config);
355     }
356 
setHelpAnimation()357     private void setHelpAnimation() {
358         final float translationX = 40;
359         final int duration = 550;
360         final RelativeLayout progressLottieLayout = findViewById(R.id.progress_lottie);
361         mHelpAnimation = ObjectAnimator.ofFloat(progressLottieLayout,
362                 "translationX" /* propertyName */,
363                 0, translationX, -1 * translationX, translationX, 0f);
364         mHelpAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
365         mHelpAnimation.setDuration(duration);
366         mHelpAnimation.setAutoCancel(false);
367     }
368     @Override
getSidecar()369     protected BiometricEnrollSidecar getSidecar() {
370         final FingerprintEnrollSidecar sidecar = new FingerprintEnrollSidecar(this,
371                 FingerprintManager.ENROLL_ENROLL);
372         return sidecar;
373     }
374 
375     @Override
shouldStartAutomatically()376     protected boolean shouldStartAutomatically() {
377         if (mCanAssumeUdfps) {
378             // Continue enrollment if restoring (e.g. configuration changed). Otherwise, wait
379             // for the entry animation to complete before starting.
380             return mRestoring && !mIsCanceled;
381         }
382         return true;
383     }
384 
385     @Override
onSaveInstanceState(Bundle outState)386     protected void onSaveInstanceState(Bundle outState) {
387         super.onSaveInstanceState(outState);
388         outState.putBoolean(KEY_STATE_CANCELED, mIsCanceled);
389         outState.putInt(KEY_STATE_PREVIOUS_ROTATION, mPreviousRotation);
390     }
391 
restoreSavedState(Bundle savedInstanceState)392     private void restoreSavedState(Bundle savedInstanceState) {
393         mRestoring = true;
394         mIsCanceled = savedInstanceState.getBoolean(KEY_STATE_CANCELED, false);
395         mPreviousRotation = savedInstanceState.getInt(KEY_STATE_PREVIOUS_ROTATION,
396                 getDisplay().getRotation());
397         mIsOrientationChanged = mPreviousRotation != getDisplay().getRotation();
398     }
399 
400     @Override
onStart()401     protected void onStart() {
402         super.onStart();
403         updateProgress(false /* animate */);
404         updateTitleAndDescription();
405         if (mRestoring) {
406             startIconAnimation();
407         }
408     }
409 
410     @Override
onEnterAnimationComplete()411     public void onEnterAnimationComplete() {
412         super.onEnterAnimationComplete();
413 
414         if (mCanAssumeUdfps) {
415             startEnrollment();
416         }
417 
418         mAnimationCancelled = false;
419         startIconAnimation();
420     }
421 
startIconAnimation()422     private void startIconAnimation() {
423         if (mIconAnimationDrawable != null) {
424             mIconAnimationDrawable.start();
425         }
426     }
427 
stopIconAnimation()428     private void stopIconAnimation() {
429         mAnimationCancelled = true;
430         if (mIconAnimationDrawable != null) {
431             mIconAnimationDrawable.stop();
432         }
433     }
434 
435     @VisibleForTesting
onCancelEnrollment(@dRes int errorMsgId)436     void onCancelEnrollment(@IdRes int errorMsgId) {
437         // showErrorDialog() will cause onWindowFocusChanged(false), set mIsCanceled to false
438         // before showErrorDialog() to prevent that another error dialog is triggered again.
439         mIsCanceled = true;
440         FingerprintErrorDialog.showErrorDialog(this, errorMsgId, mCanAssumeUdfps);
441         mIsOrientationChanged = false;
442         cancelEnrollment();
443         stopIconAnimation();
444         stopListenOrientationEvent();
445         if (!mCanAssumeUdfps) {
446             mErrorText.removeCallbacks(mTouchAgainRunnable);
447         }
448     }
449 
450     @Override
onStop()451     protected void onStop() {
452         if (!isChangingConfigurations()) {
453             if (!WizardManagerHelper.isAnySetupWizard(getIntent())
454                     && !BiometricUtils.isAnyMultiBiometricFlow(this)
455                     && !mFromSettingsSummary) {
456                 setResult(RESULT_TIMEOUT);
457             }
458             finish();
459         }
460         stopIconAnimation();
461 
462         super.onStop();
463     }
464 
465     @Override
shouldFinishWhenBackgrounded()466     protected boolean shouldFinishWhenBackgrounded() {
467         // Prevent super.onStop() from finishing, since we handle this in our onStop().
468         return false;
469     }
470 
471     @Override
onDestroy()472     protected void onDestroy() {
473         stopListenOrientationEvent();
474         super.onDestroy();
475     }
476 
animateProgress(int progress)477     private void animateProgress(int progress) {
478         if (mCanAssumeUdfps) {
479             // UDFPS animations are owned by SystemUI
480             if (progress >= PROGRESS_BAR_MAX) {
481                 // Wait for any animations in SysUI to finish, then proceed to next page
482                 getMainThreadHandler().postDelayed(mDelayedFinishRunnable, getFinishDelay());
483             }
484             return;
485         }
486         if (mProgressAnim != null) {
487             mProgressAnim.cancel();
488         }
489         ObjectAnimator anim = ObjectAnimator.ofInt(mProgressBar, "progress",
490                 mProgressBar.getProgress(), progress);
491         anim.addListener(mProgressAnimationListener);
492         anim.setInterpolator(mFastOutSlowInInterpolator);
493         anim.setDuration(250);
494         anim.start();
495         mProgressAnim = anim;
496     }
497 
animateFlash()498     private void animateFlash() {
499         if (mIconBackgroundBlinksDrawable != null) {
500             mIconBackgroundBlinksDrawable.start();
501         }
502     }
503 
getFinishIntent()504     protected Intent getFinishIntent() {
505         return new Intent(this, FingerprintEnrollFinish.class);
506     }
507 
updateTitleAndDescription()508     private void updateTitleAndDescription() {
509         if (mCanAssumeUdfps) {
510             updateTitleAndDescriptionForUdfps();
511             return;
512         } else if (mCanAssumeSfps) {
513             updateTitleAndDescriptionForSfps();
514             return;
515         }
516 
517         if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) {
518             setDescriptionText(R.string.security_settings_fingerprint_enroll_start_message);
519         } else {
520             setDescriptionText(R.string.security_settings_fingerprint_enroll_repeat_message);
521         }
522     }
523 
updateTitleAndDescriptionForUdfps()524     private void updateTitleAndDescriptionForUdfps() {
525         switch (getCurrentStage()) {
526             case STAGE_CENTER:
527                 setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title);
528                 setDescriptionText(R.string.security_settings_udfps_enroll_start_message);
529                 break;
530 
531             case STAGE_GUIDED:
532                 setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title);
533                 if (mIsAccessibilityEnabled) {
534                     setDescriptionText(R.string.security_settings_udfps_enroll_repeat_a11y_message);
535                 } else {
536                     setDescriptionText(R.string.security_settings_udfps_enroll_repeat_message);
537                 }
538                 break;
539 
540             case STAGE_FINGERTIP:
541                 setHeaderText(R.string.security_settings_udfps_enroll_fingertip_title);
542                 if (!mHaveShownUdfpsTipLottie && mIllustrationLottie != null) {
543                     mHaveShownUdfpsTipLottie = true;
544                     mIllustrationLottie.setContentDescription(
545                             getString(R.string.security_settings_udfps_tip_fingerprint_help)
546                     );
547                     configureEnrollmentStage(R.raw.udfps_tip_hint_lottie);
548                 }
549                 break;
550 
551             case STAGE_LEFT_EDGE:
552                 setHeaderText(R.string.security_settings_udfps_enroll_left_edge_title);
553                 if (!mHaveShownUdfpsLeftEdgeLottie && mIllustrationLottie != null) {
554                     mHaveShownUdfpsLeftEdgeLottie = true;
555                     mIllustrationLottie.setContentDescription(
556                             getString(R.string.security_settings_udfps_side_fingerprint_help)
557                     );
558                     configureEnrollmentStage(R.raw.udfps_left_edge_hint_lottie);
559                 } else if (mIllustrationLottie == null) {
560                     if (isStageHalfCompleted()) {
561                         setDescriptionText(
562                                 R.string.security_settings_fingerprint_enroll_repeat_message);
563                     } else {
564                         setDescriptionText(R.string.security_settings_udfps_enroll_edge_message);
565                     }
566                 }
567                 break;
568             case STAGE_RIGHT_EDGE:
569                 setHeaderText(R.string.security_settings_udfps_enroll_right_edge_title);
570                 if (!mHaveShownUdfpsRightEdgeLottie && mIllustrationLottie != null) {
571                     mHaveShownUdfpsRightEdgeLottie = true;
572                     mIllustrationLottie.setContentDescription(
573                             getString(R.string.security_settings_udfps_side_fingerprint_help)
574                     );
575                     configureEnrollmentStage(R.raw.udfps_right_edge_hint_lottie);
576 
577                 } else if (mIllustrationLottie == null) {
578                     if (isStageHalfCompleted()) {
579                         setDescriptionText(
580                                 R.string.security_settings_fingerprint_enroll_repeat_message);
581                     } else {
582                         setDescriptionText(R.string.security_settings_udfps_enroll_edge_message);
583                     }
584                 }
585                 break;
586 
587             case STAGE_UNKNOWN:
588             default:
589                 // setHeaderText(R.string.security_settings_fingerprint_enroll_udfps_title);
590                 // Don't use BiometricEnrollBase#setHeaderText, since that invokes setTitle,
591                 // which gets announced for a11y upon entering the page. For UDFPS, we want to
592                 // announce a different string for a11y upon entering the page.
593                 getLayout().setHeaderText(
594                         R.string.security_settings_fingerprint_enroll_udfps_title);
595                 setDescriptionText(R.string.security_settings_udfps_enroll_start_message);
596                 final CharSequence description = getString(
597                         R.string.security_settings_udfps_enroll_a11y);
598                 getLayout().getHeaderTextView().setContentDescription(description);
599                 setTitle(description);
600                 break;
601 
602         }
603     }
604 
605     // Interrupt any existing talkback speech to prevent stacking talkback messages
clearTalkback()606     private void clearTalkback() {
607         AccessibilityManager.getInstance(getApplicationContext()).interrupt();
608     }
609 
updateTitleAndDescriptionForSfps()610     private void updateTitleAndDescriptionForSfps() {
611         if (mIsAccessibilityEnabled) {
612             clearTalkback();
613             getLayout().getDescriptionTextView().setAccessibilityLiveRegion(
614                     View.ACCESSIBILITY_LIVE_REGION_POLITE);
615         }
616         switch (getCurrentSfpsStage()) {
617             case SFPS_STAGE_NO_ANIMATION:
618                 setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title);
619                 if (!mHaveShownSfpsNoAnimationLottie && mIllustrationLottie != null) {
620                     mHaveShownSfpsNoAnimationLottie = true;
621                     mIllustrationLottie.setContentDescription(
622                             getString(
623                                     R.string.security_settings_sfps_animation_a11y_label,
624                                     0
625                             )
626                     );
627                     configureEnrollmentStage(R.raw.sfps_lottie_no_animation);
628                 }
629                 break;
630 
631             case SFPS_STAGE_CENTER:
632                 setHeaderText(R.string.security_settings_sfps_enroll_finger_center_title);
633                 if (!mHaveShownSfpsCenterLottie && mIllustrationLottie != null) {
634                     mHaveShownSfpsCenterLottie = true;
635                     configureEnrollmentStage(R.raw.sfps_lottie_pad_center);
636                 }
637                 break;
638 
639             case SFPS_STAGE_FINGERTIP:
640                 setHeaderText(R.string.security_settings_sfps_enroll_fingertip_title);
641                 if (!mHaveShownSfpsTipLottie && mIllustrationLottie != null) {
642                     mHaveShownSfpsTipLottie = true;
643                     configureEnrollmentStage(R.raw.sfps_lottie_tip);
644                 }
645                 break;
646 
647             case SFPS_STAGE_LEFT_EDGE:
648                 setHeaderText(R.string.security_settings_sfps_enroll_left_edge_title);
649                 if (!mHaveShownSfpsLeftEdgeLottie && mIllustrationLottie != null) {
650                     mHaveShownSfpsLeftEdgeLottie = true;
651                     configureEnrollmentStage(R.raw.sfps_lottie_left_edge);
652                 }
653                 break;
654 
655             case SFPS_STAGE_RIGHT_EDGE:
656                 setHeaderText(R.string.security_settings_sfps_enroll_right_edge_title);
657                 if (!mHaveShownSfpsRightEdgeLottie && mIllustrationLottie != null) {
658                     mHaveShownSfpsRightEdgeLottie = true;
659                     configureEnrollmentStage(R.raw.sfps_lottie_right_edge);
660                 }
661                 break;
662 
663             case STAGE_UNKNOWN:
664             default:
665                 // Don't use BiometricEnrollBase#setHeaderText, since that invokes setTitle,
666                 // which gets announced for a11y upon entering the page. For SFPS, we want to
667                 // announce a different string for a11y upon entering the page.
668                 getLayout().setHeaderText(
669                         R.string.security_settings_sfps_enroll_find_sensor_title);
670                 final CharSequence description = getString(
671                         R.string.security_settings_sfps_enroll_find_sensor_message);
672                 getLayout().getHeaderTextView().setContentDescription(description);
673                 setTitle(description);
674                 break;
675 
676         }
677     }
678 
configureEnrollmentStage(@awRes int lottie)679     @VisibleForTesting void configureEnrollmentStage(@RawRes int lottie) {
680         if (!mCanAssumeSfps) {
681             setDescriptionText("");
682         }
683         LottieCompositionFactory.fromRawRes(this, lottie)
684                 .addListener((c) -> {
685                     mIllustrationLottie.setComposition(c);
686                     mIllustrationLottie.setVisibility(View.VISIBLE);
687                     mIllustrationLottie.playAnimation();
688                 });
689     }
690 
691     @EnrollStage
getCurrentStage()692     private int getCurrentStage() {
693         if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) {
694             return STAGE_UNKNOWN;
695         }
696 
697         final int progressSteps = mSidecar.getEnrollmentSteps() - mSidecar.getEnrollmentRemaining();
698         if (progressSteps < getStageThresholdSteps(0)) {
699             return STAGE_CENTER;
700         } else if (progressSteps < getStageThresholdSteps(1)) {
701             return STAGE_GUIDED;
702         } else if (progressSteps < getStageThresholdSteps(2)) {
703             return STAGE_FINGERTIP;
704         } else if (progressSteps < getStageThresholdSteps(3)) {
705             return STAGE_LEFT_EDGE;
706         } else {
707             return STAGE_RIGHT_EDGE;
708         }
709     }
710 
711     @SfpsEnrollStage
getCurrentSfpsStage()712     private int getCurrentSfpsStage() {
713         if (mSidecar == null) {
714             return STAGE_UNKNOWN;
715         }
716 
717         final int progressSteps = mSidecar.getEnrollmentSteps() - mSidecar.getEnrollmentRemaining();
718         if (progressSteps < getStageThresholdSteps(0)) {
719             return SFPS_STAGE_NO_ANIMATION;
720         } else if (progressSteps < getStageThresholdSteps(1)) {
721             return SFPS_STAGE_CENTER;
722         } else if (progressSteps < getStageThresholdSteps(2)) {
723             return SFPS_STAGE_FINGERTIP;
724         } else if (progressSteps < getStageThresholdSteps(3)) {
725             return SFPS_STAGE_LEFT_EDGE;
726         } else {
727             return SFPS_STAGE_RIGHT_EDGE;
728         }
729     }
730 
isStageHalfCompleted()731     private boolean isStageHalfCompleted() {
732         // Prior to first enrollment step.
733         if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) {
734             return false;
735         }
736 
737         final int progressSteps = mSidecar.getEnrollmentSteps() - mSidecar.getEnrollmentRemaining();
738         int prevThresholdSteps = 0;
739         for (int i = 0; i < mFingerprintManager.getEnrollStageCount(); i++) {
740             final int thresholdSteps = getStageThresholdSteps(i);
741             if (progressSteps >= prevThresholdSteps && progressSteps < thresholdSteps) {
742                 final int adjustedProgress = progressSteps - prevThresholdSteps;
743                 final int adjustedThreshold = thresholdSteps - prevThresholdSteps;
744                 return adjustedProgress >= adjustedThreshold / 2;
745             }
746             prevThresholdSteps = thresholdSteps;
747         }
748 
749         // After last enrollment step.
750         return true;
751     }
752 
753     @VisibleForTesting
getStageThresholdSteps(int index)754     protected int getStageThresholdSteps(int index) {
755         if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) {
756             Log.w(TAG, "getStageThresholdSteps: Enrollment not started yet");
757             return 1;
758         }
759         return Math.round(mSidecar.getEnrollmentSteps()
760                 * mFingerprintManager.getEnrollStageThreshold(index));
761     }
762 
763     @Override
onEnrollmentHelp(int helpMsgId, CharSequence helpString)764     public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
765         if (!TextUtils.isEmpty(helpString)) {
766             if (!(mCanAssumeUdfps || mCanAssumeSfps)) {
767                 mErrorText.removeCallbacks(mTouchAgainRunnable);
768             }
769             showError(helpString);
770         }
771     }
772 
773     @Override
onEnrollmentError(int errMsgId, CharSequence errString)774     public void onEnrollmentError(int errMsgId, CharSequence errString) {
775         onCancelEnrollment(errMsgId);
776     }
777 
announceEnrollmentProgress(CharSequence announcement)778     private void announceEnrollmentProgress(CharSequence announcement) {
779         AccessibilityEvent e = AccessibilityEvent.obtain();
780         e.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
781         e.setClassName(getClass().getName());
782         e.setPackageName(getPackageName());
783         e.getText().add(announcement);
784         mAccessibilityManager.sendAccessibilityEvent(e);
785     }
786 
787     @Override
onEnrollmentProgressChange(int steps, int remaining)788     public void onEnrollmentProgressChange(int steps, int remaining) {
789         updateProgress(true /* animate */);
790         final int percent = (int) (((float) (steps - remaining) / (float) steps) * 100);
791         if (mCanAssumeSfps && mIsAccessibilityEnabled) {
792             CharSequence announcement = getString(
793                     R.string.security_settings_sfps_enroll_progress_a11y_message, percent);
794             announceEnrollmentProgress(announcement);
795             if (mIllustrationLottie != null) {
796                 mIllustrationLottie.setContentDescription(
797                         getString(
798                                 R.string.security_settings_sfps_animation_a11y_label,
799                                 percent)
800                 );
801             }
802         }
803         updateTitleAndDescription();
804         animateFlash();
805         if (mCanAssumeUdfps) {
806             if (mIsAccessibilityEnabled) {
807                 CharSequence announcement = getString(
808                         R.string.security_settings_udfps_enroll_progress_a11y_message, percent);
809                 announceEnrollmentProgress(announcement);
810             }
811         } else if (!mCanAssumeSfps) {
812             mErrorText.removeCallbacks(mTouchAgainRunnable);
813             mErrorText.postDelayed(mTouchAgainRunnable, HINT_TIMEOUT_DURATION);
814         }
815     }
816 
updateProgress(boolean animate)817     private void updateProgress(boolean animate) {
818         if (mSidecar == null || !mSidecar.isEnrolling()) {
819             Log.d(TAG, "Enrollment not started yet");
820             return;
821         }
822 
823         int progress = getProgress(
824                 mSidecar.getEnrollmentSteps(), mSidecar.getEnrollmentRemaining());
825         // Only clear the error when progress has been made.
826         // TODO (b/234772728) Add tests.
827         if (mProgressBar != null && mProgressBar.getProgress() < progress) {
828             clearError();
829         }
830         if (animate) {
831             animateProgress(progress);
832         } else {
833             if (mProgressBar != null) {
834                 mProgressBar.setProgress(progress);
835             }
836             if (progress >= PROGRESS_BAR_MAX) {
837                 mDelayedFinishRunnable.run();
838             }
839         }
840     }
841 
getProgress(int steps, int remaining)842     private int getProgress(int steps, int remaining) {
843         if (steps == -1) {
844             return 0;
845         }
846         int progress = Math.max(0, steps + 1 - remaining);
847         return PROGRESS_BAR_MAX * progress / (steps + 1);
848     }
849 
showIconTouchDialog()850     private void showIconTouchDialog() {
851         mIconTouchCount = 0;
852         new IconTouchDialog().show(getSupportFragmentManager(), null /* tag */);
853     }
854 
showError(CharSequence error)855     private void showError(CharSequence error) {
856         if (mCanAssumeSfps) {
857             setHeaderText(error);
858             if (!mHelpAnimation.isRunning()) {
859                 mHelpAnimation.start();
860             }
861             applySfpsErrorDynamicColors(getApplicationContext(), true);
862         } else if (mCanAssumeUdfps) {
863             setHeaderText(error);
864             // Show nothing for subtitle when getting an error message.
865             setDescriptionText("");
866         } else {
867             mErrorText.setText(error);
868             if (mErrorText.getVisibility() == View.INVISIBLE) {
869                 mErrorText.setVisibility(View.VISIBLE);
870                 mErrorText.setTranslationY(getResources().getDimensionPixelSize(
871                         R.dimen.fingerprint_error_text_appear_distance));
872                 mErrorText.setAlpha(0f);
873                 mErrorText.animate()
874                         .alpha(1f)
875                         .translationY(0f)
876                         .setDuration(200)
877                         .setInterpolator(mLinearOutSlowInInterpolator)
878                         .start();
879             } else {
880                 mErrorText.animate().cancel();
881                 mErrorText.setAlpha(1f);
882                 mErrorText.setTranslationY(0f);
883             }
884         }
885         if (isResumed() && mIsAccessibilityEnabled && !mCanAssumeUdfps) {
886             mVibrator.vibrate(Process.myUid(), getApplicationContext().getOpPackageName(),
887                     VIBRATE_EFFECT_ERROR, getClass().getSimpleName() + "::showError",
888                     FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES);
889         }
890     }
891 
clearError()892     private void clearError() {
893         if (mCanAssumeSfps) {
894             applySfpsErrorDynamicColors(getApplicationContext(), false);
895         }
896         if ((!(mCanAssumeUdfps || mCanAssumeSfps)) && mErrorText.getVisibility() == View.VISIBLE) {
897             mErrorText.animate()
898                     .alpha(0f)
899                     .translationY(getResources().getDimensionPixelSize(
900                             R.dimen.fingerprint_error_text_disappear_distance))
901                     .setDuration(100)
902                     .setInterpolator(mFastOutLinearInInterpolator)
903                     .withEndAction(() -> mErrorText.setVisibility(View.INVISIBLE))
904                     .start();
905         }
906     }
907 
908     /**
909      * Applies dynamic colors corresponding to showing or clearing errors on the progress bar
910      * and finger lottie for SFPS
911      */
applySfpsErrorDynamicColors(Context context, boolean isError)912     private void applySfpsErrorDynamicColors(Context context, boolean isError) {
913         applyProgressBarDynamicColor(context, isError);
914         if (mIllustrationLottie != null) {
915             applyLottieDynamicColor(context, isError);
916         }
917     }
918 
applyProgressBarDynamicColor(Context context, boolean isError)919     private void applyProgressBarDynamicColor(Context context, boolean isError) {
920         if (mProgressBar != null) {
921             int error_color = context.getColor(R.color.sfps_enrollment_progress_bar_error_color);
922             int progress_bar_fill_color = context.getColor(
923                     R.color.sfps_enrollment_progress_bar_fill_color);
924             ColorStateList fillColor = ColorStateList.valueOf(
925                     isError ? error_color : progress_bar_fill_color);
926             mProgressBar.setProgressTintList(fillColor);
927             mProgressBar.setProgressTintMode(PorterDuff.Mode.SRC);
928             mProgressBar.invalidate();
929         }
930     }
931 
applyLottieDynamicColor(Context context, boolean isError)932     private void applyLottieDynamicColor(Context context, boolean isError) {
933         int error_color = context.getColor(R.color.sfps_enrollment_fp_error_color);
934         int fp_captured_color = context.getColor(R.color.sfps_enrollment_fp_captured_color);
935         int color = isError ? error_color : fp_captured_color;
936         mIllustrationLottie.addValueCallback(
937                 new KeyPath(".blue100", "**"),
938                 LottieProperty.COLOR_FILTER,
939                 frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
940         );
941         mIllustrationLottie.invalidate();
942     }
943 
listenOrientationEvent()944     private void listenOrientationEvent() {
945         mOrientationEventListener = new OrientationEventListener(this) {
946             @Override
947             public void onOrientationChanged(int orientation) {
948                 final int currentRotation = getDisplay().getRotation();
949                 if ((mPreviousRotation == Surface.ROTATION_90
950                         && currentRotation == Surface.ROTATION_270) || (
951                         mPreviousRotation == Surface.ROTATION_270
952                                 && currentRotation == Surface.ROTATION_90)) {
953                     mPreviousRotation = currentRotation;
954                     recreate();
955                 }
956             }
957         };
958         mOrientationEventListener.enable();
959         mPreviousRotation = getDisplay().getRotation();
960     }
961 
stopListenOrientationEvent()962     private void stopListenOrientationEvent() {
963         if (mOrientationEventListener != null) {
964             mOrientationEventListener.disable();
965         }
966         mOrientationEventListener = null;
967     }
968 
969     private final Animator.AnimatorListener mProgressAnimationListener =
970             new Animator.AnimatorListener() {
971 
972                 @Override
973                 public void onAnimationStart(Animator animation) {
974                     startIconAnimation();
975                 }
976 
977                 @Override
978                 public void onAnimationRepeat(Animator animation) { }
979 
980                 @Override
981                 public void onAnimationEnd(Animator animation) {
982                     stopIconAnimation();
983 
984                     if (mProgressBar.getProgress() >= PROGRESS_BAR_MAX) {
985                         mProgressBar.postDelayed(mDelayedFinishRunnable, getFinishDelay());
986                     }
987                 }
988 
989                 @Override
990                 public void onAnimationCancel(Animator animation) { }
991             };
992 
getFinishDelay()993     private long getFinishDelay() {
994         return mCanAssumeUdfps ? 400L : 250L;
995     }
996 
997     // Give the user a chance to see progress completed before jumping to the next stage.
998     private final Runnable mDelayedFinishRunnable = new Runnable() {
999         @Override
1000         public void run() {
1001             launchFinish(mToken);
1002         }
1003     };
1004 
1005     private final Animatable2.AnimationCallback mIconAnimationCallback =
1006             new Animatable2.AnimationCallback() {
1007         @Override
1008         public void onAnimationEnd(Drawable d) {
1009             if (mAnimationCancelled) {
1010                 return;
1011             }
1012 
1013             // Start animation after it has ended.
1014             mProgressBar.post(new Runnable() {
1015                 @Override
1016                 public void run() {
1017                     startIconAnimation();
1018                 }
1019             });
1020         }
1021     };
1022 
1023     private final Runnable mShowDialogRunnable = new Runnable() {
1024         @Override
1025         public void run() {
1026             showIconTouchDialog();
1027         }
1028     };
1029 
1030     private final Runnable mTouchAgainRunnable = new Runnable() {
1031         @Override
1032         public void run() {
1033             showError(getString(R.string.security_settings_fingerprint_enroll_lift_touch_again));
1034         }
1035     };
1036 
1037     @Override
getMetricsCategory()1038     public int getMetricsCategory() {
1039         return SettingsEnums.FINGERPRINT_ENROLLING;
1040     }
1041 
updateOrientation(int orientation)1042     private void updateOrientation(int orientation) {
1043         if (mCanAssumeSfps) {
1044             mIllustrationLottie = findViewById(R.id.illustration_lottie);
1045         } else {
1046             switch(orientation) {
1047                 case Configuration.ORIENTATION_LANDSCAPE: {
1048                     mIllustrationLottie = null;
1049                     break;
1050                 }
1051                 case Configuration.ORIENTATION_PORTRAIT: {
1052                     if (mShouldShowLottie) {
1053                         mIllustrationLottie = findViewById(R.id.illustration_lottie);
1054                     }
1055                     break;
1056                 }
1057                 default:
1058                     Log.e(TAG, "Error unhandled configuration change");
1059                     break;
1060             }
1061         }
1062     }
1063 
1064     @Override
onConfigurationChanged(@onNull Configuration newConfig)1065     public void onConfigurationChanged(@NonNull Configuration newConfig) {
1066         maybeHideSfpsText(newConfig);
1067         switch(newConfig.orientation) {
1068             case Configuration.ORIENTATION_LANDSCAPE: {
1069                 updateOrientation(Configuration.ORIENTATION_LANDSCAPE);
1070                 break;
1071             }
1072             case Configuration.ORIENTATION_PORTRAIT: {
1073                 updateOrientation(Configuration.ORIENTATION_PORTRAIT);
1074                 break;
1075             }
1076             default:
1077                 Log.e(TAG, "Error unhandled configuration change");
1078                 break;
1079         }
1080     }
1081 
maybeHideSfpsText(@onNull Configuration newConfig)1082     private void maybeHideSfpsText(@NonNull Configuration newConfig) {
1083         final HeaderMixin headerMixin = getLayout().getMixin(HeaderMixin.class);
1084         final DescriptionMixin descriptionMixin = getLayout().getMixin(DescriptionMixin.class);
1085         final boolean isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
1086 
1087         if (mCanAssumeSfps) {
1088             // hide the description
1089             descriptionMixin.getTextView().setVisibility(View.GONE);
1090             headerMixin.getTextView().setHyphenationFrequency(HYPHENATION_FREQUENCY_NONE);
1091             if (isLandscape) {
1092                 headerMixin.setAutoTextSizeEnabled(true);
1093                 headerMixin.getTextView().setMinLines(0);
1094                 headerMixin.getTextView().setMaxLines(10);
1095             } else {
1096                 headerMixin.setAutoTextSizeEnabled(false);
1097                 headerMixin.getTextView().setLines(4);
1098             }
1099         }
1100     }
1101 
1102     public static class IconTouchDialog extends InstrumentedDialogFragment {
1103 
1104         @Override
onCreateDialog(Bundle savedInstanceState)1105         public Dialog onCreateDialog(Bundle savedInstanceState) {
1106             AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(),
1107                     R.style.Theme_AlertDialog);
1108             builder.setTitle(R.string.security_settings_fingerprint_enroll_touch_dialog_title)
1109                     .setMessage(R.string.security_settings_fingerprint_enroll_touch_dialog_message)
1110                     .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok,
1111                             new DialogInterface.OnClickListener() {
1112                                 @Override
1113                                 public void onClick(DialogInterface dialog, int which) {
1114                                     dialog.dismiss();
1115                                 }
1116                             });
1117             return builder.create();
1118         }
1119 
1120         @Override
getMetricsCategory()1121         public int getMetricsCategory() {
1122             return SettingsEnums.DIALOG_FINGERPRINT_ICON_TOUCH;
1123         }
1124     }
1125 }