• 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.text.Layout.HYPHENATION_FREQUENCY_NORMAL;
20 
21 import android.app.settings.SettingsEnums;
22 import android.content.Intent;
23 import android.content.res.Configuration;
24 import android.content.res.Resources;
25 import android.hardware.fingerprint.FingerprintManager;
26 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
27 import android.os.Bundle;
28 import android.util.Log;
29 import android.view.OrientationEventListener;
30 import android.view.Surface;
31 import android.view.View;
32 import android.view.accessibility.AccessibilityManager;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.Nullable;
36 
37 import com.android.settings.R;
38 import com.android.settings.Utils;
39 import com.android.settings.biometrics.BiometricEnrollBase;
40 import com.android.settings.biometrics.BiometricEnrollSidecar;
41 import com.android.settings.biometrics.BiometricUtils;
42 import com.android.settings.flags.Flags;
43 import com.android.settings.overlay.FeatureFactory;
44 import com.android.settings.password.ChooseLockSettingsHelper;
45 import com.android.settingslib.widget.LottieColorUtils;
46 import com.android.systemui.unfold.compat.ScreenSizeFoldProvider;
47 import com.android.systemui.unfold.updates.FoldProvider;
48 
49 import com.airbnb.lottie.LottieAnimationView;
50 import com.google.android.setupcompat.template.FooterBarMixin;
51 import com.google.android.setupcompat.template.FooterButton;
52 
53 import java.util.List;
54 
55 /**
56  * Activity explaining the fingerprint sensor location for fingerprint enrollment.
57  */
58 public class FingerprintEnrollFindSensor extends BiometricEnrollBase implements
59         BiometricEnrollSidecar.Listener, FoldProvider.FoldCallback {
60 
61     private static final String TAG = "FingerprintEnrollFindSensor";
62     private static final String SAVED_STATE_IS_NEXT_CLICKED = "is_next_clicked";
63 
64     @Nullable
65     private FingerprintFindSensorAnimation mAnimation;
66 
67     @Nullable
68     private LottieAnimationView mIllustrationLottie;
69 
70     private FingerprintEnrollSidecar mSidecar;
71     private boolean mNextClicked;
72     private boolean mCanAssumeUdfps;
73     private boolean mCanAssumeSfps;
74 
75     private OrientationEventListener mOrientationEventListener;
76     private int mPreviousRotation = 0;
77     private ScreenSizeFoldProvider mScreenSizeFoldProvider;
78     private boolean mIsFolded;
79     private boolean mIsReverseDefaultRotation;
80     @Nullable
81     protected UdfpsEnrollCalibrator mCalibrator;
82 
83     @Override
onCreate(Bundle savedInstanceState)84     protected void onCreate(Bundle savedInstanceState) {
85         super.onCreate(savedInstanceState);
86 
87         final FingerprintManager fingerprintManager = Utils.getFingerprintManagerOrNull(this);
88         final List<FingerprintSensorPropertiesInternal> props =
89                 fingerprintManager.getSensorPropertiesInternal();
90         mCanAssumeUdfps = props != null && props.size() == 1 && props.get(0).isAnyUdfpsType();
91         mCanAssumeSfps = props != null && props.size() == 1 && props.get(0).isAnySidefpsType();
92         setContentView(getContentView());
93         mScreenSizeFoldProvider = new ScreenSizeFoldProvider(getApplicationContext());
94         mScreenSizeFoldProvider.registerCallback(this, getApplicationContext().getMainExecutor());
95         mScreenSizeFoldProvider
96                 .onConfigurationChange(getApplicationContext().getResources().getConfiguration());
97         mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class);
98         mFooterBarMixin.setSecondaryButton(
99                 new FooterButton.Builder(this)
100                         .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
101                         .setListener(this::onSkipButtonClick)
102                         .setButtonType(FooterButton.ButtonType.SKIP)
103                         .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary)
104                         .build()
105         );
106         getLayout().getHeaderTextView().setHyphenationFrequency(HYPHENATION_FREQUENCY_NORMAL);
107 
108         listenOrientationEvent();
109 
110         if (mCanAssumeUdfps) {
111             setHeaderText(R.string.security_settings_udfps_enroll_find_sensor_title);
112             setDescriptionText(R.string.security_settings_udfps_enroll_find_sensor_message);
113 
114             mIllustrationLottie = findViewById(R.id.illustration_lottie);
115             AccessibilityManager am = getSystemService(AccessibilityManager.class);
116             if (am.isEnabled()) {
117                 mIllustrationLottie.setAnimation(R.raw.udfps_edu_a11y_lottie);
118             }
119         } else if (mCanAssumeSfps) {
120             setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title);
121             setDescriptionText(R.string.security_settings_sfps_enroll_find_sensor_message);
122             mIsReverseDefaultRotation = getApplicationContext().getResources().getBoolean(
123                     com.android.internal.R.bool.config_reverseDefaultRotation);
124         } else {
125             setHeaderText(R.string.security_settings_fingerprint_enroll_find_sensor_title);
126             setDescriptionText(R.string.security_settings_fingerprint_enroll_find_sensor_message);
127         }
128         if (savedInstanceState != null) {
129             mNextClicked = savedInstanceState.getBoolean(SAVED_STATE_IS_NEXT_CLICKED, mNextClicked);
130         }
131 
132         // This is an entry point for SetNewPasswordController, e.g.
133         // adb shell am start -a android.app.action.SET_NEW_PASSWORD
134         if (mToken == null && BiometricUtils.containsGatekeeperPasswordHandle(getIntent())) {
135             fingerprintManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> {
136                 mChallenge = challenge;
137                 mSensorId = sensorId;
138                 mToken = BiometricUtils.requestGatekeeperHat(this, getIntent(), mUserId, challenge);
139 
140                 // Put this into the intent. This is really just to work around the fact that the
141                 // enrollment sidecar gets the HAT from the activity's intent, rather than having
142                 // it passed in.
143                 getIntent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken);
144 
145                 // Do not start looking for fingerprint if this activity is re-created because it is
146                 // waiting for activity result from enrolling activity.
147                 if (!mNextClicked) {
148                     startLookingForFingerprint();
149                 }
150             });
151         } else if (mToken != null) {
152             // Do not start looking for fingerprint if this activity is re-created because it is
153             // waiting for activity result from enrolling activity.
154             if (!mNextClicked) {
155                 // HAT passed in from somewhere else, such as FingerprintEnrollIntroduction
156                 startLookingForFingerprint();
157             }
158         } else {
159             // There's something wrong with the enrollment flow, this should never happen.
160             throw new IllegalStateException("HAT and GkPwHandle both missing...");
161         }
162 
163         mAnimation = null;
164         if (mCanAssumeUdfps) {
165             if (Flags.udfpsEnrollCalibration()) {
166                 mCalibrator = FeatureFactory.getFeatureFactory().getFingerprintFeatureProvider()
167                         .getUdfpsEnrollCalibrator(getApplicationContext(), savedInstanceState,
168                                 getIntent());
169                 if (mCalibrator != null) {
170                     mCalibrator.onWaitingPage(
171                             getLifecycle(),
172                             getSupportFragmentManager(),
173                             this::enableUdfpsLottieAndNextButton
174                     );
175                 } else {
176                     enableUdfpsLottieAndNextButton();
177                 }
178             } else {
179                 enableUdfpsLottieAndNextButton();
180             }
181         } else if (!mCanAssumeSfps) {
182             View animationView = findViewById(R.id.fingerprint_sensor_location_animation);
183             if (animationView instanceof FingerprintFindSensorAnimation) {
184                 mAnimation = (FingerprintFindSensorAnimation) animationView;
185             }
186         }
187     }
188 
enableUdfpsLottieAndNextButton()189     private void enableUdfpsLottieAndNextButton() {
190         if (isFinishing()) {
191             return;
192         }
193 
194         if (mFooterBarMixin.getPrimaryButton() == null) {
195             mFooterBarMixin.setPrimaryButton(new FooterButton.Builder(this)
196                     .setText(R.string.security_settings_udfps_enroll_find_sensor_start_button)
197                     .setListener(this::onStartButtonClick)
198                     .setButtonType(FooterButton.ButtonType.NEXT)
199                     .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
200                     .build()
201             );
202         }
203         if (mIllustrationLottie != null) {
204             mIllustrationLottie.setOnClickListener(this::onStartButtonClick);
205         }
206     }
207 
getRotationFromDefault(int rotation)208     private int getRotationFromDefault(int rotation) {
209         if (mIsReverseDefaultRotation) {
210             return (rotation + 1) % 4;
211         } else {
212             return rotation;
213         }
214     }
215 
updateSfpsFindSensorAnimationAsset()216     private void updateSfpsFindSensorAnimationAsset() {
217         mScreenSizeFoldProvider
218                 .onConfigurationChange(getApplicationContext().getResources().getConfiguration());
219         mIllustrationLottie = findViewById(R.id.illustration_lottie);
220         final int rotation = getRotationFromDefault(
221                 getApplicationContext().getDisplay().getRotation());
222 
223         switch (rotation) {
224             case Surface.ROTATION_90:
225                 if (mIsFolded) {
226                     mIllustrationLottie.setAnimation(
227                             R.raw.fingerprint_edu_lottie_folded_top_left);
228                 } else {
229                     mIllustrationLottie.setAnimation(
230                             R.raw.fingerprint_edu_lottie_portrait_top_left);
231                 }
232                 break;
233             case Surface.ROTATION_180:
234                 if (mIsFolded) {
235                     mIllustrationLottie.setAnimation(
236                             R.raw.fingerprint_edu_lottie_folded_bottom_left);
237                 } else {
238                     mIllustrationLottie.setAnimation(
239                             R.raw.fingerprint_edu_lottie_landscape_bottom_left);
240                 }
241                 break;
242             case Surface.ROTATION_270:
243                 if (mIsFolded) {
244                     mIllustrationLottie.setAnimation(
245                             R.raw.fingerprint_edu_lottie_folded_bottom_right);
246                 } else {
247                     mIllustrationLottie.setAnimation(
248                             R.raw.fingerprint_edu_lottie_portrait_bottom_right);
249                 }
250                 break;
251             default:
252                 if (mIsFolded) {
253                     mIllustrationLottie.setAnimation(
254                             R.raw.fingerprint_edu_lottie_folded_top_right);
255                 } else {
256                     mIllustrationLottie.setAnimation(
257                             R.raw.fingerprint_edu_lottie_landscape_top_right);
258                 }
259                 break;
260         }
261 
262         LottieColorUtils.applyDynamicColors(getApplicationContext(), mIllustrationLottie);
263         mIllustrationLottie.setVisibility(View.VISIBLE);
264         mIllustrationLottie.playAnimation();
265     }
266 
267     @Override
onConfigurationChanged(@onNull Configuration newConfig)268     public void onConfigurationChanged(@NonNull Configuration newConfig) {
269         super.onConfigurationChanged(newConfig);
270         mScreenSizeFoldProvider.onConfigurationChange(newConfig);
271     }
272 
273     @Override
onResume()274     protected void onResume() {
275         super.onResume();
276         if (mCanAssumeSfps) {
277             updateSfpsFindSensorAnimationAsset();
278         }
279     }
280 
281     @Override
onSaveInstanceState(Bundle outState)282     protected void onSaveInstanceState(Bundle outState) {
283         super.onSaveInstanceState(outState);
284         outState.putBoolean(SAVED_STATE_IS_NEXT_CLICKED, mNextClicked);
285         if (Flags.udfpsEnrollCalibration()) {
286             if (mCalibrator != null) {
287                 mCalibrator.onSaveInstanceState(outState);
288             }
289         }
290     }
291 
292     @Override
getFingerprintEnrollingIntent()293     protected Intent getFingerprintEnrollingIntent() {
294         final Intent ret = super.getFingerprintEnrollingIntent();
295         ret.putExtra(BiometricUtils.EXTRA_ENROLL_REASON,
296                 getIntent().getIntExtra(BiometricUtils.EXTRA_ENROLL_REASON, -1));
297         if (Flags.udfpsEnrollCalibration()) {
298             if (mCalibrator != null) {
299                 ret.putExtras(mCalibrator.getExtrasForNextIntent());
300             }
301         }
302         return ret;
303     }
304 
305     @Override
onBackPressed()306     public void onBackPressed() {
307         stopLookingForFingerprint();
308         super.onBackPressed();
309     }
310 
311     @Override
onApplyThemeResource(Resources.Theme theme, int resid, boolean first)312     protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {
313         theme.applyStyle(R.style.SetupWizardPartnerResource, true);
314         super.onApplyThemeResource(theme, resid, first);
315     }
316 
getContentView()317     protected int getContentView() {
318         if (mCanAssumeUdfps) {
319             return R.layout.udfps_enroll_find_sensor_layout;
320         } else if (mCanAssumeSfps) {
321             return R.layout.sfps_enroll_find_sensor_layout;
322         }
323         return R.layout.fingerprint_enroll_find_sensor;
324     }
325 
326     @Override
onStart()327     protected void onStart() {
328         super.onStart();
329         if (mAnimation != null) {
330             mAnimation.startAnimation();
331         }
332     }
333 
stopLookingForFingerprint()334     private void stopLookingForFingerprint() {
335         if (mSidecar != null) {
336             mSidecar.setListener(null);
337             mSidecar.cancelEnrollment();
338             getSupportFragmentManager()
339                     .beginTransaction().remove(mSidecar).commitAllowingStateLoss();
340             mSidecar = null;
341         }
342     }
343 
startLookingForFingerprint()344     private void startLookingForFingerprint() {
345         if (mCanAssumeUdfps) {
346             // UDFPS devices use this screen as an educational screen. Users should tap the
347             // "Start" button to move to the next screen to begin enrollment.
348             return;
349         }
350         mSidecar = (FingerprintEnrollSidecar) getSupportFragmentManager().findFragmentByTag(
351                 FingerprintEnrollEnrolling.TAG_SIDECAR);
352         if (mSidecar == null) {
353             mSidecar = new FingerprintEnrollSidecar(this,
354                     FingerprintManager.ENROLL_FIND_SENSOR, getIntent());
355             getSupportFragmentManager().beginTransaction()
356                     .add(mSidecar, FingerprintEnrollEnrolling.TAG_SIDECAR)
357                     .commitAllowingStateLoss();
358         }
359         mSidecar.setListener(this);
360     }
361 
362     @Override
onEnrollmentProgressChange(int steps, int remaining)363     public void onEnrollmentProgressChange(int steps, int remaining) {
364         mNextClicked = true;
365         proceedToEnrolling(true /* cancelEnrollment */);
366     }
367 
368     @Override
onEnrollmentHelp(int helpMsgId, CharSequence helpString)369     public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
370     }
371 
372     @Override
onEnrollmentError(int errMsgId, CharSequence errString)373     public void onEnrollmentError(int errMsgId, CharSequence errString) {
374         if (mNextClicked && errMsgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
375             proceedToEnrolling(false /* cancelEnrollment */);
376         } else {
377             FingerprintErrorDialog.showErrorDialog(this, errMsgId,
378                     this instanceof SetupFingerprintEnrollFindSensor);
379         }
380     }
381 
382     @Override
onStop()383     protected void onStop() {
384         super.onStop();
385         mScreenSizeFoldProvider.unregisterCallback(this);
386         if (mAnimation != null) {
387             mAnimation.pauseAnimation();
388         }
389     }
390 
391     @Override
shouldFinishWhenBackgrounded()392     protected boolean shouldFinishWhenBackgrounded() {
393         return super.shouldFinishWhenBackgrounded() && !mNextClicked;
394     }
395 
396     @Override
onDestroy()397     protected void onDestroy() {
398         stopListenOrientationEvent();
399         super.onDestroy();
400         if (mAnimation != null) {
401             mAnimation.stopAnimation();
402         }
403     }
404 
onStartButtonClick(View view)405     private void onStartButtonClick(View view) {
406         mNextClicked = true;
407         startActivityForResult(getFingerprintEnrollingIntent(), ENROLL_REQUEST);
408     }
409 
onSkipButtonClick(View view)410     protected void onSkipButtonClick(View view) {
411         stopLookingForFingerprint();
412         setResult(RESULT_SKIP);
413         finish();
414     }
415 
proceedToEnrolling(boolean cancelEnrollment)416     private void proceedToEnrolling(boolean cancelEnrollment) {
417         if (mSidecar != null) {
418             if (cancelEnrollment) {
419                 if (mSidecar.cancelEnrollment()) {
420                     // Enrollment cancel requested. When the cancellation is successful,
421                     // onEnrollmentError will be called with FINGERPRINT_ERROR_CANCELED, calling
422                     // this again.
423                     return;
424                 }
425             }
426             mSidecar.setListener(null);
427             getSupportFragmentManager().beginTransaction().remove(mSidecar).
428                     commitAllowingStateLoss();
429             mSidecar = null;
430             startActivityForResult(getFingerprintEnrollingIntent(), ENROLL_REQUEST);
431         }
432     }
433 
434     @Override
onActivityResult(int requestCode, int resultCode, Intent data)435     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
436         Log.d(TAG,
437                 "onActivityResult(requestCode=" + requestCode + ", resultCode=" + resultCode + ")");
438         boolean enrolledFingerprint = false;
439         if (data != null) {
440             enrolledFingerprint = data.getBooleanExtra(EXTRA_FINISHED_ENROLL_FINGERPRINT, false);
441         }
442 
443         if (resultCode == RESULT_CANCELED && enrolledFingerprint) {
444             setResult(resultCode, data);
445             finish();
446             return;
447         }
448 
449         if (requestCode == CONFIRM_REQUEST) {
450             if (resultCode == RESULT_OK && data != null) {
451                 throw new IllegalStateException("Pretty sure this is dead code");
452                 /*
453                 mToken = data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
454                 overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
455                 getIntent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken);
456                 startLookingForFingerprint();
457                 */
458             } else {
459                 finish();
460             }
461         } else if (requestCode == ENROLL_REQUEST) {
462             switch (resultCode) {
463                 case RESULT_FINISHED:
464                 case RESULT_SKIP:
465                 case RESULT_TIMEOUT:
466                     setResult(resultCode);
467                     finish();
468                     break;
469                 default:
470                     FingerprintManager fpm = Utils.getFingerprintManagerOrNull(this);
471                     int enrolled = fpm.getEnrolledFingerprints().size();
472                     final List<FingerprintSensorPropertiesInternal> props =
473                             fpm.getSensorPropertiesInternal();
474                     final int maxEnrollments = props.get(0).maxEnrollmentsPerUser;
475                     if (enrolled >= maxEnrollments) {
476                         finish();
477                     } else {
478                         // We came back from enrolling but it wasn't completed, start again.
479                         mNextClicked = false;
480                         startLookingForFingerprint();
481                     }
482                     break;
483             }
484         } else {
485             super.onActivityResult(requestCode, resultCode, data);
486         }
487     }
488 
489     @Override
getMetricsCategory()490     public int getMetricsCategory() {
491         return SettingsEnums.FINGERPRINT_FIND_SENSOR;
492     }
493 
listenOrientationEvent()494     private void listenOrientationEvent() {
495         if (!mCanAssumeSfps) {
496             // Do nothing if the device doesn't support SideFPS.
497             return;
498         }
499         mOrientationEventListener = new OrientationEventListener(this) {
500             @Override
501             public void onOrientationChanged(int orientation) {
502                 final int currentRotation = getRotationFromDefault(getDisplay().getRotation());
503                 if ((currentRotation + 2) % 4 == mPreviousRotation) {
504                     mPreviousRotation = currentRotation;
505                     recreate();
506                 }
507             }
508         };
509         mOrientationEventListener.enable();
510         mPreviousRotation = getRotationFromDefault(getDisplay().getRotation());
511     }
512 
stopListenOrientationEvent()513     private void stopListenOrientationEvent() {
514         if (!mCanAssumeSfps) {
515             // Do nothing if the device doesn't support SideFPS.
516             return;
517         }
518         if (mOrientationEventListener != null) {
519             mOrientationEventListener.disable();
520         }
521         mOrientationEventListener = null;
522     }
523 
524     @Override
onFoldUpdated(boolean isFolded)525     public void onFoldUpdated(boolean isFolded) {
526         Log.d(TAG, "onFoldUpdated= " + isFolded);
527         mIsFolded = isFolded;
528     }
529 }
530