• 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 android.animation.Animator;
20 import android.animation.ObjectAnimator;
21 import android.annotation.Nullable;
22 import android.app.Dialog;
23 import android.app.settings.SettingsEnums;
24 import android.content.DialogInterface;
25 import android.content.Intent;
26 import android.graphics.drawable.Animatable2;
27 import android.graphics.drawable.AnimatedVectorDrawable;
28 import android.graphics.drawable.Drawable;
29 import android.graphics.drawable.LayerDrawable;
30 import android.hardware.fingerprint.FingerprintManager;
31 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
32 import android.media.AudioAttributes;
33 import android.os.Bundle;
34 import android.os.VibrationEffect;
35 import android.os.Vibrator;
36 import android.text.TextUtils;
37 import android.util.Log;
38 import android.view.MotionEvent;
39 import android.view.OrientationEventListener;
40 import android.view.Surface;
41 import android.view.View;
42 import android.view.accessibility.AccessibilityEvent;
43 import android.view.accessibility.AccessibilityManager;
44 import android.view.animation.AnimationUtils;
45 import android.view.animation.Interpolator;
46 import android.widget.ProgressBar;
47 import android.widget.TextView;
48 
49 import androidx.appcompat.app.AlertDialog;
50 
51 import com.android.settings.R;
52 import com.android.settings.biometrics.BiometricEnrollSidecar;
53 import com.android.settings.biometrics.BiometricUtils;
54 import com.android.settings.biometrics.BiometricsEnrollEnrolling;
55 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
56 
57 import com.google.android.setupcompat.template.FooterBarMixin;
58 import com.google.android.setupcompat.template.FooterButton;
59 import com.google.android.setupcompat.util.WizardManagerHelper;
60 
61 import java.util.List;
62 
63 /**
64  * Activity which handles the actual enrolling for fingerprint.
65  */
66 public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
67 
68     private static final String TAG = "FingerprintEnrollEnrolling";
69     static final String TAG_SIDECAR = "sidecar";
70 
71     private static final int PROGRESS_BAR_MAX = 10000;
72     private static final int FINISH_DELAY = 250;
73     /**
74      * Enroll with two center touches before going to guided enrollment.
75      */
76     private static final int NUM_CENTER_TOUCHES = 2;
77 
78     /**
79      * If we don't see progress during this time, we show an error message to remind the users that
80      * they need to lift the finger and touch again.
81      */
82     private static final int HINT_TIMEOUT_DURATION = 2500;
83 
84     /**
85      * How long the user needs to touch the icon until we show the dialog.
86      */
87     private static final long ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN = 500;
88 
89     /**
90      * How many times the user needs to touch the icon until we show the dialog that this is not the
91      * fingerprint sensor.
92      */
93     private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3;
94 
95     private static final VibrationEffect VIBRATE_EFFECT_ERROR =
96             VibrationEffect.createWaveform(new long[] {0, 5, 55, 60}, -1);
97     private static final AudioAttributes FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES =
98             new AudioAttributes.Builder()
99                     .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
100                     .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
101                     .build();
102 
103     private boolean mCanAssumeUdfps;
104     @Nullable private ProgressBar mProgressBar;
105     private ObjectAnimator mProgressAnim;
106     private TextView mDescriptionText;
107     private TextView mErrorText;
108     private Interpolator mFastOutSlowInInterpolator;
109     private Interpolator mLinearOutSlowInInterpolator;
110     private Interpolator mFastOutLinearInInterpolator;
111     private int mIconTouchCount;
112     private boolean mAnimationCancelled;
113     @Nullable private AnimatedVectorDrawable mIconAnimationDrawable;
114     @Nullable private AnimatedVectorDrawable mIconBackgroundBlinksDrawable;
115     private boolean mRestoring;
116     private Vibrator mVibrator;
117     private boolean mIsSetupWizard;
118     private AccessibilityManager mAccessibilityManager;
119     private boolean mIsAccessibilityEnabled;
120 
121     private OrientationEventListener mOrientationEventListener;
122     private int mPreviousRotation = 0;
123 
124     @Override
onCreate(Bundle savedInstanceState)125     protected void onCreate(Bundle savedInstanceState) {
126         super.onCreate(savedInstanceState);
127 
128         final FingerprintManager fingerprintManager = getSystemService(FingerprintManager.class);
129         final List<FingerprintSensorPropertiesInternal> props =
130                 fingerprintManager.getSensorPropertiesInternal();
131         mCanAssumeUdfps = props.size() == 1 && props.get(0).isAnyUdfpsType();
132 
133         mAccessibilityManager = getSystemService(AccessibilityManager.class);
134         mIsAccessibilityEnabled = mAccessibilityManager.isEnabled();
135 
136         listenOrientationEvent();
137 
138         if (mCanAssumeUdfps) {
139             if (BiometricUtils.isReverseLandscape(getApplicationContext())) {
140                 setContentView(R.layout.udfps_enroll_enrolling_land);
141             } else {
142                 setContentView(R.layout.udfps_enroll_enrolling);
143             }
144             setDescriptionText(R.string.security_settings_udfps_enroll_start_message);
145         } else {
146             setContentView(R.layout.fingerprint_enroll_enrolling);
147             setDescriptionText(R.string.security_settings_fingerprint_enroll_start_message);
148         }
149 
150         mIsSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent());
151         if (mCanAssumeUdfps) {
152             updateTitleAndDescription();
153         } else {
154             setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title);
155         }
156 
157         mErrorText = findViewById(R.id.error_text);
158         mProgressBar = findViewById(R.id.fingerprint_progress_bar);
159         mVibrator = getSystemService(Vibrator.class);
160 
161         mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class);
162         mFooterBarMixin.setSecondaryButton(
163                 new FooterButton.Builder(this)
164                         .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
165                         .setListener(this::onSkipButtonClick)
166                         .setButtonType(FooterButton.ButtonType.SKIP)
167                         .setTheme(R.style.SudGlifButton_Secondary)
168                         .build()
169         );
170 
171         final LayerDrawable fingerprintDrawable = mProgressBar != null
172                 ? (LayerDrawable) mProgressBar.getBackground() : null;
173         if (fingerprintDrawable != null) {
174             mIconAnimationDrawable = (AnimatedVectorDrawable)
175                     fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_animation);
176             mIconBackgroundBlinksDrawable = (AnimatedVectorDrawable)
177                     fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_background);
178             mIconAnimationDrawable.registerAnimationCallback(mIconAnimationCallback);
179         }
180 
181         mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
182                 this, android.R.interpolator.fast_out_slow_in);
183         mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
184                 this, android.R.interpolator.linear_out_slow_in);
185         mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
186                 this, android.R.interpolator.fast_out_linear_in);
187         if (mProgressBar != null) {
188             mProgressBar.setOnTouchListener((v, event) -> {
189                 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
190                     mIconTouchCount++;
191                     if (mIconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) {
192                         showIconTouchDialog();
193                     } else {
194                         mProgressBar.postDelayed(mShowDialogRunnable,
195                                 ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN);
196                     }
197                 } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
198                         || event.getActionMasked() == MotionEvent.ACTION_UP) {
199                     mProgressBar.removeCallbacks(mShowDialogRunnable);
200                 }
201                 return true;
202             });
203         }
204         mRestoring = savedInstanceState != null;
205     }
206 
207     @Override
getSidecar()208     protected BiometricEnrollSidecar getSidecar() {
209         final FingerprintEnrollSidecar sidecar = new FingerprintEnrollSidecar();
210         sidecar.setEnrollReason(FingerprintManager.ENROLL_ENROLL);
211         return sidecar;
212     }
213 
214     @Override
shouldStartAutomatically()215     protected boolean shouldStartAutomatically() {
216         if (mCanAssumeUdfps) {
217             // Continue enrollment if restoring (e.g. configuration changed). Otherwise, wait
218             // for the entry animation to complete before starting.
219             return mRestoring;
220         }
221         return true;
222     }
223 
224     @Override
onStart()225     protected void onStart() {
226         super.onStart();
227         updateProgress(false /* animate */);
228         updateTitleAndDescription();
229         if (mRestoring) {
230             startIconAnimation();
231         }
232     }
233 
234     @Override
onEnterAnimationComplete()235     public void onEnterAnimationComplete() {
236         super.onEnterAnimationComplete();
237 
238         if (mCanAssumeUdfps) {
239             startEnrollment();
240         }
241 
242         mAnimationCancelled = false;
243         startIconAnimation();
244     }
245 
startIconAnimation()246     private void startIconAnimation() {
247         if (mIconAnimationDrawable != null) {
248             mIconAnimationDrawable.start();
249         }
250     }
251 
stopIconAnimation()252     private void stopIconAnimation() {
253         mAnimationCancelled = true;
254         if (mIconAnimationDrawable != null) {
255             mIconAnimationDrawable.stop();
256         }
257     }
258 
259     @Override
onStop()260     protected void onStop() {
261         super.onStop();
262         stopIconAnimation();
263     }
264 
265     @Override
onDestroy()266     protected void onDestroy() {
267         stopListenOrientationEvent();
268         super.onDestroy();
269     }
270 
animateProgress(int progress)271     private void animateProgress(int progress) {
272         if (mCanAssumeUdfps) {
273             // UDFPS animations are owned by SystemUI
274             if (progress >= PROGRESS_BAR_MAX) {
275                 // Wait for any animations in SysUI to finish, then proceed to next page
276                 getMainThreadHandler().postDelayed(mDelayedFinishRunnable, FINISH_DELAY);
277             }
278             return;
279         }
280         if (mProgressAnim != null) {
281             mProgressAnim.cancel();
282         }
283         ObjectAnimator anim = ObjectAnimator.ofInt(mProgressBar, "progress",
284                 mProgressBar.getProgress(), progress);
285         anim.addListener(mProgressAnimationListener);
286         anim.setInterpolator(mFastOutSlowInInterpolator);
287         anim.setDuration(250);
288         anim.start();
289         mProgressAnim = anim;
290     }
291 
animateFlash()292     private void animateFlash() {
293         if (mIconBackgroundBlinksDrawable != null) {
294             mIconBackgroundBlinksDrawable.start();
295         }
296     }
297 
getFinishIntent()298     protected Intent getFinishIntent() {
299         return new Intent(this, FingerprintEnrollFinish.class);
300     }
301 
updateTitleAndDescription()302     private void updateTitleAndDescription() {
303         if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) {
304             if (mCanAssumeUdfps) {
305                 // setHeaderText(R.string.security_settings_fingerprint_enroll_udfps_title);
306                 // Don't use BiometricEnrollBase#setHeaderText, since that invokes setTitle,
307                 // which gets announced for a11y upon entering the page. For UDFPS, we want to
308                 // announce a different string for a11y upon entering the page.
309                 getLayout().setHeaderText(
310                         R.string.security_settings_fingerprint_enroll_udfps_title);
311                 setDescriptionText(R.string.security_settings_udfps_enroll_start_message);
312 
313                 final CharSequence description = getString(
314                         R.string.security_settings_udfps_enroll_a11y);
315                 getLayout().getHeaderTextView().setContentDescription(description);
316                 setTitle(description);
317             } else {
318                 setDescriptionText(R.string.security_settings_fingerprint_enroll_start_message);
319             }
320         } else if (mCanAssumeUdfps && !isCenterEnrollmentComplete()) {
321             if (mIsSetupWizard) {
322                 setHeaderText(R.string.security_settings_udfps_enroll_title_one_more_time);
323             } else {
324                 setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title);
325             }
326             setDescriptionText(R.string.security_settings_udfps_enroll_start_message);
327         } else {
328             if (mCanAssumeUdfps) {
329                 setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title);
330                 if (mIsAccessibilityEnabled) {
331                     setDescriptionText(R.string.security_settings_udfps_enroll_repeat_a11y_message);
332                 } else {
333                     setDescriptionText(R.string.security_settings_udfps_enroll_repeat_message);
334                 }
335             } else {
336                 setDescriptionText(R.string.security_settings_fingerprint_enroll_repeat_message);
337             }
338         }
339     }
340 
isCenterEnrollmentComplete()341     private boolean isCenterEnrollmentComplete() {
342         if (mSidecar == null || mSidecar.getEnrollmentSteps() == -1) {
343             return false;
344         }
345         final int stepsEnrolled = mSidecar.getEnrollmentSteps() - mSidecar.getEnrollmentRemaining();
346         return stepsEnrolled >= NUM_CENTER_TOUCHES;
347     }
348 
349     @Override
onEnrollmentHelp(int helpMsgId, CharSequence helpString)350     public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
351         if (!TextUtils.isEmpty(helpString)) {
352             if (!mCanAssumeUdfps) {
353                 mErrorText.removeCallbacks(mTouchAgainRunnable);
354             }
355             showError(helpString);
356         }
357     }
358 
359     @Override
onEnrollmentError(int errMsgId, CharSequence errString)360     public void onEnrollmentError(int errMsgId, CharSequence errString) {
361         FingerprintErrorDialog.showErrorDialog(this, errMsgId);
362         stopIconAnimation();
363         if (!mCanAssumeUdfps) {
364             mErrorText.removeCallbacks(mTouchAgainRunnable);
365         }
366     }
367 
368     @Override
onEnrollmentProgressChange(int steps, int remaining)369     public void onEnrollmentProgressChange(int steps, int remaining) {
370         updateProgress(true /* animate */);
371         updateTitleAndDescription();
372         clearError();
373         animateFlash();
374         if (!mCanAssumeUdfps) {
375             mErrorText.removeCallbacks(mTouchAgainRunnable);
376             mErrorText.postDelayed(mTouchAgainRunnable, HINT_TIMEOUT_DURATION);
377         } else {
378             if (mIsAccessibilityEnabled) {
379                 final int percent = (int) (((float)(steps - remaining) / (float) steps) * 100);
380                 CharSequence cs = getString(
381                         R.string.security_settings_udfps_enroll_progress_a11y_message, percent);
382                 AccessibilityEvent e = AccessibilityEvent.obtain();
383                 e.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
384                 e.setClassName(getClass().getName());
385                 e.setPackageName(getPackageName());
386                 e.getText().add(cs);
387                 mAccessibilityManager.sendAccessibilityEvent(e);
388             }
389         }
390     }
391 
updateProgress(boolean animate)392     private void updateProgress(boolean animate) {
393         if (mSidecar == null || !mSidecar.isEnrolling()) {
394             Log.d(TAG, "Enrollment not started yet");
395             return;
396         }
397 
398         int progress = getProgress(
399                 mSidecar.getEnrollmentSteps(), mSidecar.getEnrollmentRemaining());
400         if (animate) {
401             animateProgress(progress);
402         } else {
403             if (mProgressBar != null) {
404                 mProgressBar.setProgress(progress);
405             }
406             if (progress >= PROGRESS_BAR_MAX) {
407                 mDelayedFinishRunnable.run();
408             }
409         }
410     }
411 
getProgress(int steps, int remaining)412     private int getProgress(int steps, int remaining) {
413         if (steps == -1) {
414             return 0;
415         }
416         int progress = Math.max(0, steps + 1 - remaining);
417         return PROGRESS_BAR_MAX * progress / (steps + 1);
418     }
419 
showIconTouchDialog()420     private void showIconTouchDialog() {
421         mIconTouchCount = 0;
422         new IconTouchDialog().show(getSupportFragmentManager(), null /* tag */);
423     }
424 
showError(CharSequence error)425     private void showError(CharSequence error) {
426         if (mCanAssumeUdfps) {
427             setHeaderText(error);
428             // Show nothing for subtitle when getting an error message.
429             setDescriptionText("");
430         } else {
431             mErrorText.setText(error);
432             if (mErrorText.getVisibility() == View.INVISIBLE) {
433                 mErrorText.setVisibility(View.VISIBLE);
434                 mErrorText.setTranslationY(getResources().getDimensionPixelSize(
435                         R.dimen.fingerprint_error_text_appear_distance));
436                 mErrorText.setAlpha(0f);
437                 mErrorText.animate()
438                         .alpha(1f)
439                         .translationY(0f)
440                         .setDuration(200)
441                         .setInterpolator(mLinearOutSlowInInterpolator)
442                         .start();
443             } else {
444                 mErrorText.animate().cancel();
445                 mErrorText.setAlpha(1f);
446                 mErrorText.setTranslationY(0f);
447             }
448         }
449         if (isResumed()) {
450             mVibrator.vibrate(VIBRATE_EFFECT_ERROR, FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES);
451         }
452     }
453 
clearError()454     private void clearError() {
455         if (!mCanAssumeUdfps && mErrorText.getVisibility() == View.VISIBLE) {
456             mErrorText.animate()
457                     .alpha(0f)
458                     .translationY(getResources().getDimensionPixelSize(
459                             R.dimen.fingerprint_error_text_disappear_distance))
460                     .setDuration(100)
461                     .setInterpolator(mFastOutLinearInInterpolator)
462                     .withEndAction(() -> mErrorText.setVisibility(View.INVISIBLE))
463                     .start();
464         }
465     }
466 
listenOrientationEvent()467     private void listenOrientationEvent() {
468         mOrientationEventListener = new OrientationEventListener(this) {
469             @Override
470             public void onOrientationChanged(int orientation) {
471                 final int currentRotation = getDisplay().getRotation();
472                 if ((mPreviousRotation == Surface.ROTATION_90
473                         && currentRotation == Surface.ROTATION_270) || (
474                         mPreviousRotation == Surface.ROTATION_270
475                                 && currentRotation == Surface.ROTATION_90)) {
476                     mPreviousRotation = currentRotation;
477                     recreate();
478                 }
479             }
480         };
481         mOrientationEventListener.enable();
482         mPreviousRotation = getDisplay().getRotation();
483     }
484 
stopListenOrientationEvent()485     private void stopListenOrientationEvent() {
486         if (mOrientationEventListener != null) {
487             mOrientationEventListener.disable();
488         }
489         mOrientationEventListener = null;
490     }
491 
492     private final Animator.AnimatorListener mProgressAnimationListener
493             = new Animator.AnimatorListener() {
494 
495         @Override
496         public void onAnimationStart(Animator animation) { }
497 
498         @Override
499         public void onAnimationRepeat(Animator animation) { }
500 
501         @Override
502         public void onAnimationEnd(Animator animation) {
503             if (mProgressBar.getProgress() >= PROGRESS_BAR_MAX) {
504                 mProgressBar.postDelayed(mDelayedFinishRunnable, FINISH_DELAY);
505             }
506         }
507 
508         @Override
509         public void onAnimationCancel(Animator animation) { }
510     };
511 
512     // Give the user a chance to see progress completed before jumping to the next stage.
513     private final Runnable mDelayedFinishRunnable = new Runnable() {
514         @Override
515         public void run() {
516             launchFinish(mToken);
517         }
518     };
519 
520     private final Animatable2.AnimationCallback mIconAnimationCallback =
521             new Animatable2.AnimationCallback() {
522         @Override
523         public void onAnimationEnd(Drawable d) {
524             if (mAnimationCancelled) {
525                 return;
526             }
527 
528             // Start animation after it has ended.
529             mProgressBar.post(new Runnable() {
530                 @Override
531                 public void run() {
532                     startIconAnimation();
533                 }
534             });
535         }
536     };
537 
538     private final Runnable mShowDialogRunnable = new Runnable() {
539         @Override
540         public void run() {
541             showIconTouchDialog();
542         }
543     };
544 
545     private final Runnable mTouchAgainRunnable = new Runnable() {
546         @Override
547         public void run() {
548             showError(getString(R.string.security_settings_fingerprint_enroll_lift_touch_again));
549         }
550     };
551 
552     @Override
getMetricsCategory()553     public int getMetricsCategory() {
554         return SettingsEnums.FINGERPRINT_ENROLLING;
555     }
556 
557     public static class IconTouchDialog extends InstrumentedDialogFragment {
558 
559         @Override
onCreateDialog(Bundle savedInstanceState)560         public Dialog onCreateDialog(Bundle savedInstanceState) {
561             AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
562             builder.setTitle(R.string.security_settings_fingerprint_enroll_touch_dialog_title)
563                     .setMessage(R.string.security_settings_fingerprint_enroll_touch_dialog_message)
564                     .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok,
565                             new DialogInterface.OnClickListener() {
566                                 @Override
567                                 public void onClick(DialogInterface dialog, int which) {
568                                     dialog.dismiss();
569                                 }
570                             });
571             return builder.create();
572         }
573 
574         @Override
getMetricsCategory()575         public int getMetricsCategory() {
576             return SettingsEnums.DIALOG_FINGERPRINT_ICON_TOUCH;
577         }
578     }
579 }
580