• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.settings.biometrics;
18 
19 import android.app.admin.DevicePolicyManager;
20 import android.content.Intent;
21 import android.graphics.PorterDuff;
22 import android.graphics.PorterDuffColorFilter;
23 import android.hardware.biometrics.BiometricAuthenticator;
24 import android.os.Bundle;
25 import android.os.UserHandle;
26 import android.os.UserManager;
27 import android.util.Log;
28 import android.view.View;
29 import android.widget.TextView;
30 
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.annotation.StringRes;
34 import androidx.annotation.VisibleForTesting;
35 
36 import com.android.internal.widget.LockPatternUtils;
37 import com.android.settings.R;
38 import com.android.settings.SetupWizardUtils;
39 import com.android.settings.password.ChooseLockGeneric;
40 import com.android.settings.password.ChooseLockSettingsHelper;
41 import com.android.settings.password.SetupSkipDialog;
42 
43 import com.google.android.setupcompat.template.FooterBarMixin;
44 import com.google.android.setupcompat.template.FooterButton;
45 import com.google.android.setupcompat.util.WizardManagerHelper;
46 import com.google.android.setupdesign.GlifLayout;
47 import com.google.android.setupdesign.span.LinkSpan;
48 import com.google.android.setupdesign.template.RequireScrollMixin;
49 import com.google.android.setupdesign.util.DynamicColorPalette;
50 
51 /**
52  * Abstract base class for the intro onboarding activity for biometric enrollment.
53  */
54 public abstract class BiometricEnrollIntroduction extends BiometricEnrollBase
55         implements LinkSpan.OnClickListener {
56 
57     private static final String TAG = "BiometricEnrollIntroduction";
58 
59     private static final String KEY_CONFIRMING_CREDENTIALS = "confirming_credentials";
60     private static final String KEY_SCROLLED_TO_BOTTOM = "scrolled";
61 
62     private GatekeeperPasswordProvider mGatekeeperPasswordProvider;
63     private UserManager mUserManager;
64     private boolean mHasPassword;
65     private boolean mBiometricUnlockDisabledByAdmin;
66     private TextView mErrorText;
67     protected boolean mConfirmingCredentials;
68     protected boolean mNextClicked;
69     private boolean mParentalConsentRequired;
70     private boolean mHasScrolledToBottom = false;
71 
72     @Nullable private PorterDuffColorFilter mIconColorFilter;
73 
74     /**
75      * @return true if the biometric is disabled by a device administrator
76      */
isDisabledByAdmin()77     protected abstract boolean isDisabledByAdmin();
78 
79     /**
80      * @return the layout resource
81      */
getLayoutResource()82     protected abstract int getLayoutResource();
83 
84     /**
85      * @return the header resource for if the biometric has been disabled by a device administrator
86      */
getHeaderResDisabledByAdmin()87     protected abstract int getHeaderResDisabledByAdmin();
88 
89     /**
90      * @return the default header resource
91      */
getHeaderResDefault()92     protected abstract int getHeaderResDefault();
93 
94     /**
95      * @return the description for if the biometric has been disabled by a device admin
96      */
getDescriptionDisabledByAdmin()97     protected abstract String getDescriptionDisabledByAdmin();
98 
99     /**
100      * @return the cancel button
101      */
getCancelButton()102     protected abstract FooterButton getCancelButton();
103 
104     /**
105      * @return the next button
106      */
getNextButton()107     protected abstract FooterButton getNextButton();
108 
109     /**
110      * @return the error TextView
111      */
getErrorTextView()112     protected abstract TextView getErrorTextView();
113 
114     /**
115      * @return 0 if there are no errors, otherwise returns the resource ID for the error string
116      * to be displayed.
117      */
checkMaxEnrolled()118     protected abstract int checkMaxEnrolled();
119 
120     /**
121      * @return the challenge generated by the biometric hardware
122      */
getChallenge(GenerateChallengeCallback callback)123     protected abstract void getChallenge(GenerateChallengeCallback callback);
124 
125     /**
126      * @return one of the ChooseLockSettingsHelper#EXTRA_KEY_FOR_* constants
127      */
getExtraKeyForBiometric()128     protected abstract String getExtraKeyForBiometric();
129 
130     /**
131      * @return the intent for proceeding to the next step of enrollment. For Fingerprint, this
132      * should lead to the "Find Sensor" activity. For Face, this should lead to the "Enrolling"
133      * activity.
134      */
getEnrollingIntent()135     protected abstract Intent getEnrollingIntent();
136 
137     /**
138      * @return the title to be shown on the ConfirmLock screen.
139      */
getConfirmLockTitleResId()140     protected abstract int getConfirmLockTitleResId();
141 
142     /**
143      * @param span
144      */
onClick(LinkSpan span)145     public abstract void onClick(LinkSpan span);
146 
getModality()147     public abstract @BiometricAuthenticator.Modality int getModality();
148 
149     protected interface GenerateChallengeCallback {
onChallengeGenerated(int sensorId, int userId, long challenge)150         void onChallengeGenerated(int sensorId, int userId, long challenge);
151     }
152 
153     @Override
onCreate(Bundle savedInstanceState)154     protected void onCreate(Bundle savedInstanceState) {
155         super.onCreate(savedInstanceState);
156 
157         if (savedInstanceState != null) {
158             mConfirmingCredentials = savedInstanceState.getBoolean(KEY_CONFIRMING_CREDENTIALS);
159             mHasScrolledToBottom = savedInstanceState.getBoolean(KEY_SCROLLED_TO_BOTTOM);
160             mLaunchedPostureGuidance = savedInstanceState.getBoolean(
161                     EXTRA_LAUNCHED_POSTURE_GUIDANCE);
162         }
163 
164         Intent intent = getIntent();
165         if (intent.getStringExtra(WizardManagerHelper.EXTRA_THEME) == null) {
166             // Put the theme in the intent so it gets propagated to other activities in the flow
167             intent.putExtra(
168                     WizardManagerHelper.EXTRA_THEME,
169                     SetupWizardUtils.getThemeString(intent));
170         }
171 
172         mBiometricUnlockDisabledByAdmin = isDisabledByAdmin();
173 
174         setContentView(getLayoutResource());
175         mParentalConsentRequired = ParentalControlsUtils.parentConsentRequired(this, getModality())
176                 != null;
177         if (mBiometricUnlockDisabledByAdmin && !mParentalConsentRequired) {
178             setHeaderText(getHeaderResDisabledByAdmin());
179         } else {
180             setHeaderText(getHeaderResDefault());
181         }
182 
183         mErrorText = getErrorTextView();
184 
185         mUserManager = getUserManager();
186         updatePasswordQuality();
187 
188         // Check isFinishing() because FaceEnrollIntroduction may finish self to launch
189         // FaceSettings during onCreate()
190         if (!mConfirmingCredentials && !isFinishing()) {
191             if (!mHasPassword) {
192                 // No password registered, launch into enrollment wizard.
193                 mConfirmingCredentials = true;
194                 launchChooseLock();
195             } else if (!BiometricUtils.containsGatekeeperPasswordHandle(getIntent())
196                     && mToken == null) {
197                 // It's possible to have a token but mLaunchedConfirmLock == false, since
198                 // ChooseLockGeneric can pass us a token.
199                 mConfirmingCredentials = true;
200                 launchConfirmLock(getConfirmLockTitleResId());
201             }
202         }
203 
204         final GlifLayout layout = getLayout();
205         mFooterBarMixin = layout.getMixin(FooterBarMixin.class);
206         mFooterBarMixin.setPrimaryButton(getPrimaryFooterButton());
207         mFooterBarMixin.setSecondaryButton(getSecondaryFooterButton(), true /* usePrimaryStyle */);
208         mFooterBarMixin.getSecondaryButton().setVisibility(
209                 mHasScrolledToBottom ? View.VISIBLE : View.INVISIBLE);
210 
211         final RequireScrollMixin requireScrollMixin = layout.getMixin(RequireScrollMixin.class);
212         requireScrollMixin.requireScrollWithButton(this, getPrimaryFooterButton(),
213                 getMoreButtonTextRes(), this::onNextButtonClick);
214         requireScrollMixin.setOnRequireScrollStateChangedListener(
215                 scrollNeeded -> {
216                     boolean enrollmentCompleted = checkMaxEnrolled() != 0;
217                     if (!enrollmentCompleted) {
218                         // Update text of primary button from "More" to "Agree".
219                         final int primaryButtonTextRes = scrollNeeded
220                                 ? getMoreButtonTextRes()
221                                 : getAgreeButtonTextRes();
222                         getPrimaryFooterButton().setText(this, primaryButtonTextRes);
223                     }
224 
225                     // Show secondary button once scroll is completed.
226                     if (!scrollNeeded) {
227                         if (!enrollmentCompleted) {
228                             getSecondaryFooterButton().setVisibility(View.VISIBLE);
229                         }
230                         mHasScrolledToBottom = true;
231                     }
232                 });
233     }
234 
235     @Override
onResume()236     protected void onResume() {
237         super.onResume();
238 
239         //reset mNextClick to make sure introduction page would be closed correctly
240         mNextClicked = false;
241 
242         final int errorMsg = checkMaxEnrolled();
243         if (errorMsg == 0) {
244             mErrorText.setText(null);
245             mErrorText.setVisibility(View.GONE);
246             getNextButton().setVisibility(View.VISIBLE);
247         } else {
248             mErrorText.setText(errorMsg);
249             mErrorText.setVisibility(View.VISIBLE);
250             getNextButton().setText(getResources().getString(R.string.done));
251             getNextButton().setVisibility(View.VISIBLE);
252             getSecondaryFooterButton().setVisibility(View.INVISIBLE);
253         }
254     }
255 
256     @Override
onSaveInstanceState(Bundle outState)257     protected void onSaveInstanceState(Bundle outState) {
258         super.onSaveInstanceState(outState);
259         outState.putBoolean(KEY_CONFIRMING_CREDENTIALS, mConfirmingCredentials);
260         outState.putBoolean(KEY_SCROLLED_TO_BOTTOM, mHasScrolledToBottom);
261     }
262 
263     @Override
shouldFinishWhenBackgrounded()264     protected boolean shouldFinishWhenBackgrounded() {
265         return super.shouldFinishWhenBackgrounded() && !mConfirmingCredentials && !mNextClicked;
266     }
267 
268     @VisibleForTesting
269     @NonNull
getGatekeeperPasswordProvider()270     protected GatekeeperPasswordProvider getGatekeeperPasswordProvider() {
271         if (mGatekeeperPasswordProvider == null) {
272             mGatekeeperPasswordProvider = new GatekeeperPasswordProvider(getLockPatternUtils());
273         }
274         return mGatekeeperPasswordProvider;
275     }
276 
277     @VisibleForTesting
getUserManager()278     protected UserManager getUserManager() {
279         return UserManager.get(this);
280     }
281 
282     @VisibleForTesting
283     @NonNull
getLockPatternUtils()284     protected LockPatternUtils getLockPatternUtils() {
285         return new LockPatternUtils(this);
286     }
287 
updatePasswordQuality()288     private void updatePasswordQuality() {
289         final int passwordQuality = getLockPatternUtils()
290                 .getActivePasswordQuality(mUserManager.getCredentialOwnerProfile(mUserId));
291         mHasPassword = passwordQuality != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
292     }
293 
294     @Override
onNextButtonClick(View view)295     protected void onNextButtonClick(View view) {
296         mNextClicked = true;
297         if (checkMaxEnrolled() == 0) {
298             // Lock thingy is already set up, launch directly to the next page
299             launchNextEnrollingActivity(mToken);
300         } else {
301             boolean couldStartNextBiometric = BiometricUtils.tryStartingNextBiometricEnroll(this,
302                     ENROLL_NEXT_BIOMETRIC_REQUEST, "enrollIntroduction#onNextButtonClicked");
303             if (!couldStartNextBiometric) {
304                 setResult(RESULT_FINISHED);
305                 finish();
306             }
307         }
308         mNextLaunched = true;
309     }
310 
launchChooseLock()311     private void launchChooseLock() {
312         Intent intent = BiometricUtils.getChooseLockIntent(this, getIntent());
313         intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true);
314         intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true);
315         intent.putExtra(getExtraKeyForBiometric(), true);
316         if (mUserId != UserHandle.USER_NULL) {
317             intent.putExtra(Intent.EXTRA_USER_ID, mUserId);
318         }
319         startActivityForResult(intent, CHOOSE_LOCK_GENERIC_REQUEST);
320     }
321 
launchNextEnrollingActivity(byte[] token)322     private void launchNextEnrollingActivity(byte[] token) {
323         Intent intent = getEnrollingIntent();
324         if (token != null) {
325             intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token);
326         }
327         if (mUserId != UserHandle.USER_NULL) {
328             intent.putExtra(Intent.EXTRA_USER_ID, mUserId);
329         }
330         BiometricUtils.copyMultiBiometricExtras(getIntent(), intent);
331         intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, mFromSettingsSummary);
332         intent.putExtra(EXTRA_KEY_CHALLENGE, mChallenge);
333         intent.putExtra(EXTRA_KEY_SENSOR_ID, mSensorId);
334         startActivityForResult(intent, BIOMETRIC_FIND_SENSOR_REQUEST);
335     }
336 
337     /**
338      * Returns the intent extra data for setResult(), null means nothing need to been sent back
339      */
340     @Nullable
getSetResultIntentExtra(@ullable Intent activityResultIntent)341     protected Intent getSetResultIntentExtra(@Nullable Intent activityResultIntent) {
342         return activityResultIntent;
343     }
344 
345     @Override
onActivityResult(int requestCode, int resultCode, Intent data)346     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
347         Log.d(TAG,
348                 "onActivityResult(requestCode=" + requestCode + ", resultCode=" + resultCode + ")");
349         final boolean cameFromMultiBioFpAuthAddAnother =
350                 requestCode == BiometricUtils.REQUEST_ADD_ANOTHER
351                 && BiometricUtils.isMultiBiometricFingerprintEnrollmentFlow(this);
352         if (requestCode == BIOMETRIC_FIND_SENSOR_REQUEST) {
353             if (isResultFinished(resultCode)) {
354                 handleBiometricResultSkipOrFinished(resultCode, getSetResultIntentExtra(data));
355             } else if (isResultSkipped(resultCode)) {
356                 if (!BiometricUtils.tryStartingNextBiometricEnroll(this,
357                         ENROLL_NEXT_BIOMETRIC_REQUEST, "BIOMETRIC_FIND_SENSOR_SKIPPED")) {
358                     handleBiometricResultSkipOrFinished(resultCode, data);
359                 }
360             } else if (resultCode == RESULT_TIMEOUT) {
361                 setResult(resultCode, data);
362                 finish();
363             }
364         } else if (requestCode == CHOOSE_LOCK_GENERIC_REQUEST) {
365             mConfirmingCredentials = false;
366             if (resultCode == RESULT_FINISHED) {
367                 updatePasswordQuality();
368                 final boolean handled = onSetOrConfirmCredentials(data);
369                 if (!handled) {
370                     overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
371                     getNextButton().setEnabled(false);
372                     getChallenge(((sensorId, userId, challenge) -> {
373                         mSensorId = sensorId;
374                         mChallenge = challenge;
375                         mToken = BiometricUtils.requestGatekeeperHat(this, data, mUserId,
376                                 challenge);
377                         BiometricUtils.removeGatekeeperPasswordHandle(this, data);
378                         getNextButton().setEnabled(true);
379                     }));
380                 }
381             } else {
382                 setResult(resultCode, data);
383                 finish();
384             }
385         } else if (requestCode == CONFIRM_REQUEST) {
386             mConfirmingCredentials = false;
387             if (resultCode == RESULT_OK && data != null) {
388                 final boolean handled = onSetOrConfirmCredentials(data);
389                 if (!handled) {
390                     overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
391                     getNextButton().setEnabled(false);
392                     getChallenge(((sensorId, userId, challenge) -> {
393                         mSensorId = sensorId;
394                         mChallenge = challenge;
395                         mToken = BiometricUtils.requestGatekeeperHat(this, data, mUserId,
396                                 challenge);
397                         BiometricUtils.removeGatekeeperPasswordHandle(this, data);
398                         getNextButton().setEnabled(true);
399                     }));
400                 }
401             } else {
402                 setResult(resultCode, data);
403                 finish();
404             }
405         } else if (requestCode == LEARN_MORE_REQUEST) {
406             overridePendingTransition(R.anim.sud_slide_back_in, R.anim.sud_slide_back_out);
407         } else if (requestCode == ENROLL_NEXT_BIOMETRIC_REQUEST
408                 || cameFromMultiBioFpAuthAddAnother) {
409             if (isResultFinished(resultCode)) {
410                 handleBiometricResultSkipOrFinished(resultCode, data);
411             } else if (isResultSkipped(resultCode)) {
412                 if (requestCode == BiometricUtils.REQUEST_ADD_ANOTHER) {
413                     // If we came from an add another request, it still might
414                     // be possible to add another biometric. Check if we can.
415                     if (checkMaxEnrolled() != 0) {
416                         // If we can't enroll any more biometrics, than skip
417                         // this one.
418                         handleBiometricResultSkipOrFinished(resultCode, data);
419                     }
420                 } else {
421                     handleBiometricResultSkipOrFinished(resultCode, data);
422                 }
423             } else if (resultCode != RESULT_CANCELED) {
424                 setResult(resultCode, data);
425                 finish();
426             }
427         }
428         super.onActivityResult(requestCode, resultCode, data);
429     }
430 
isResultSkipped(int resultCode)431     private static boolean isResultSkipped(int resultCode) {
432         return resultCode == RESULT_SKIP
433                 || resultCode == SetupSkipDialog.RESULT_SKIP;
434     }
435 
isResultFinished(int resultCode)436     private static boolean isResultFinished(int resultCode) {
437         return resultCode == RESULT_FINISHED;
438     }
439 
isResultSkipOrFinished(int resultCode)440     private static boolean isResultSkipOrFinished(int resultCode) {
441         return isResultSkipped(resultCode) || isResultFinished(resultCode);
442     }
443 
removeEnrollNextBiometric()444     protected void removeEnrollNextBiometric() {
445         getIntent().removeExtra(MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FACE);
446         getIntent().removeExtra(MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FINGERPRINT);
447     }
448 
removeEnrollNextBiometricIfSkipEnroll(@ullable Intent data)449     protected void removeEnrollNextBiometricIfSkipEnroll(@Nullable Intent data) {
450         if (data != null
451                 && data.getBooleanExtra(
452                         MultiBiometricEnrollHelper.EXTRA_SKIP_PENDING_ENROLL, false)) {
453             removeEnrollNextBiometric();
454         }
455     }
handleBiometricResultSkipOrFinished(int resultCode, @Nullable Intent data)456     protected void handleBiometricResultSkipOrFinished(int resultCode, @Nullable Intent data) {
457         removeEnrollNextBiometricIfSkipEnroll(data);
458         if (resultCode == RESULT_SKIP) {
459             onEnrollmentSkipped(data);
460         } else if (resultCode == RESULT_FINISHED) {
461             onFinishedEnrolling(data);
462         }
463     }
464 
465     /**
466      * Called after confirming credentials. Can be used to prevent the default
467      * behavior of immediately calling #getChallenge (useful to things like intro
468      * consent screens that don't actually do enrollment and will later start an
469      * activity that does).
470      *
471      * @return True if the default behavior should be skipped and handled by this method instead.
472      */
onSetOrConfirmCredentials(@ullable Intent data)473     protected boolean onSetOrConfirmCredentials(@Nullable Intent data) {
474         return false;
475     }
476 
onCancelButtonClick(View view)477     protected void onCancelButtonClick(View view) {
478         finish();
479     }
480 
onSkipButtonClick(View view)481     protected void onSkipButtonClick(View view) {
482         onEnrollmentSkipped(null /* data */);
483     }
484 
onEnrollmentSkipped(@ullable Intent data)485     protected void onEnrollmentSkipped(@Nullable Intent data) {
486         setResult(RESULT_SKIP, data);
487         finish();
488     }
489 
onFinishedEnrolling(@ullable Intent data)490     protected void onFinishedEnrolling(@Nullable Intent data) {
491         setResult(RESULT_FINISHED, data);
492         finish();
493     }
494 
updateDescriptionText()495     protected void updateDescriptionText() {
496         if (mBiometricUnlockDisabledByAdmin && !mParentalConsentRequired) {
497             setDescriptionText(getDescriptionDisabledByAdmin());
498         }
499     }
500 
501     @Override
initViews()502     protected void initViews() {
503         super.initViews();
504         updateDescriptionText();
505     }
506 
507     @NonNull
getIconColorFilter()508     protected PorterDuffColorFilter getIconColorFilter() {
509         if (mIconColorFilter == null) {
510             mIconColorFilter = new PorterDuffColorFilter(
511                     DynamicColorPalette.getColor(this, DynamicColorPalette.ColorType.ACCENT),
512                     PorterDuff.Mode.SRC_IN);
513         }
514         return mIconColorFilter;
515     }
516 
517     @NonNull
getPrimaryFooterButton()518     protected abstract FooterButton getPrimaryFooterButton();
519 
520     @NonNull
getSecondaryFooterButton()521     protected abstract FooterButton getSecondaryFooterButton();
522 
523     @StringRes
getAgreeButtonTextRes()524     protected abstract int getAgreeButtonTextRes();
525 
526     @StringRes
getMoreButtonTextRes()527     protected abstract int getMoreButtonTextRes();
528 }
529