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 (shouldShowSplitScreenDialog()) {
158             BiometricsSplitScreenDialog
159                     .newInstance(getModality(), !WizardManagerHelper.isAnySetupWizard(getIntent()))
160                     .show(getSupportFragmentManager(), BiometricsSplitScreenDialog.class.getName());
161         }
162 
163         if (savedInstanceState != null) {
164             mConfirmingCredentials = savedInstanceState.getBoolean(KEY_CONFIRMING_CREDENTIALS);
165             mHasScrolledToBottom = savedInstanceState.getBoolean(KEY_SCROLLED_TO_BOTTOM);
166             mLaunchedPostureGuidance = savedInstanceState.getBoolean(
167                     EXTRA_LAUNCHED_POSTURE_GUIDANCE);
168         }
169 
170         Intent intent = getIntent();
171         if (intent.getStringExtra(WizardManagerHelper.EXTRA_THEME) == null) {
172             // Put the theme in the intent so it gets propagated to other activities in the flow
173             intent.putExtra(
174                     WizardManagerHelper.EXTRA_THEME,
175                     SetupWizardUtils.getThemeString(intent));
176         }
177 
178         mBiometricUnlockDisabledByAdmin = isDisabledByAdmin();
179 
180         setContentView(getLayoutResource());
181         mParentalConsentRequired = ParentalControlsUtils.parentConsentRequired(this, getModality())
182                 != null;
183         if (mBiometricUnlockDisabledByAdmin && !mParentalConsentRequired) {
184             setHeaderText(getHeaderResDisabledByAdmin());
185         } else {
186             setHeaderText(getHeaderResDefault());
187         }
188 
189         mErrorText = getErrorTextView();
190 
191         mUserManager = getUserManager();
192         updatePasswordQuality();
193 
194         // Check isFinishing() because FaceEnrollIntroduction may finish self to launch
195         // FaceSettings during onCreate()
196         if (!mConfirmingCredentials && !isFinishing()) {
197             if (!mHasPassword) {
198                 // No password registered, launch into enrollment wizard.
199                 mConfirmingCredentials = true;
200                 launchChooseLock();
201             } else if (!BiometricUtils.containsGatekeeperPasswordHandle(getIntent())
202                     && mToken == null) {
203                 // It's possible to have a token but mLaunchedConfirmLock == false, since
204                 // ChooseLockGeneric can pass us a token.
205                 mConfirmingCredentials = true;
206                 launchConfirmLock(getConfirmLockTitleResId());
207             }
208         }
209 
210         final GlifLayout layout = getLayout();
211         mFooterBarMixin = layout.getMixin(FooterBarMixin.class);
212         mFooterBarMixin.setPrimaryButton(getPrimaryFooterButton());
213         mFooterBarMixin.setSecondaryButton(getSecondaryFooterButton(), true /* usePrimaryStyle */);
214         mFooterBarMixin.getSecondaryButton().setVisibility(
215                 mHasScrolledToBottom ? View.VISIBLE : View.INVISIBLE);
216 
217         final RequireScrollMixin requireScrollMixin = layout.getMixin(RequireScrollMixin.class);
218         requireScrollMixin.requireScrollWithButton(this, getPrimaryFooterButton(),
219                 getMoreButtonTextRes(), this::onNextButtonClick);
220         requireScrollMixin.setOnRequireScrollStateChangedListener(
221                 scrollNeeded -> {
222                     boolean enrollmentCompleted = checkMaxEnrolled() != 0;
223                     if (!enrollmentCompleted) {
224                         // Update text of primary button from "More" to "Agree".
225                         final int primaryButtonTextRes = scrollNeeded
226                                 ? getMoreButtonTextRes()
227                                 : getAgreeButtonTextRes();
228                         getPrimaryFooterButton().setText(this, primaryButtonTextRes);
229                     }
230 
231                     // Show secondary button once scroll is completed.
232                     getSecondaryFooterButton().setVisibility(
233                             !scrollNeeded && !enrollmentCompleted ? View.VISIBLE : View.INVISIBLE);
234                     mHasScrolledToBottom = !scrollNeeded;
235                 });
236 
237         final boolean isScrollNeeded = requireScrollMixin.isScrollingRequired();
238         final boolean enrollmentCompleted = checkMaxEnrolled() != 0;
239         getSecondaryFooterButton().setVisibility(
240                 !isScrollNeeded && !enrollmentCompleted ? View.VISIBLE : View.INVISIBLE);
241     }
242 
243     @Override
onResume()244     protected void onResume() {
245         super.onResume();
246 
247         //reset mNextClick to make sure introduction page would be closed correctly
248         mNextClicked = false;
249 
250         final int errorMsg = checkMaxEnrolled();
251         if (errorMsg == 0) {
252             mErrorText.setText(null);
253             mErrorText.setVisibility(View.GONE);
254             getNextButton().setVisibility(View.VISIBLE);
255         } else {
256             mErrorText.setText(errorMsg);
257             mErrorText.setVisibility(View.VISIBLE);
258             getNextButton().setText(getResources().getString(R.string.done));
259             getNextButton().setVisibility(View.VISIBLE);
260             getSecondaryFooterButton().setVisibility(View.INVISIBLE);
261         }
262     }
263 
264     @Override
onSaveInstanceState(Bundle outState)265     protected void onSaveInstanceState(Bundle outState) {
266         super.onSaveInstanceState(outState);
267         outState.putBoolean(KEY_CONFIRMING_CREDENTIALS, mConfirmingCredentials);
268         outState.putBoolean(KEY_SCROLLED_TO_BOTTOM, mHasScrolledToBottom);
269     }
270 
271     @Override
shouldFinishWhenBackgrounded()272     protected boolean shouldFinishWhenBackgrounded() {
273         return super.shouldFinishWhenBackgrounded() && !mConfirmingCredentials && !mNextClicked;
274     }
275 
276     @VisibleForTesting
277     @NonNull
getGatekeeperPasswordProvider()278     protected GatekeeperPasswordProvider getGatekeeperPasswordProvider() {
279         if (mGatekeeperPasswordProvider == null) {
280             mGatekeeperPasswordProvider = new GatekeeperPasswordProvider(getLockPatternUtils());
281         }
282         return mGatekeeperPasswordProvider;
283     }
284 
285     @VisibleForTesting
getUserManager()286     protected UserManager getUserManager() {
287         return UserManager.get(this);
288     }
289 
290     @VisibleForTesting
291     @NonNull
getLockPatternUtils()292     protected LockPatternUtils getLockPatternUtils() {
293         return new LockPatternUtils(this);
294     }
295 
updatePasswordQuality()296     private void updatePasswordQuality() {
297         final int passwordQuality = getLockPatternUtils()
298                 .getActivePasswordQuality(mUserManager.getCredentialOwnerProfile(mUserId));
299         mHasPassword = passwordQuality != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
300     }
301 
302     @Override
onNextButtonClick(View view)303     protected void onNextButtonClick(View view) {
304         // If it's not on suw, this method shouldn't be accessed.
305         if (shouldShowSplitScreenDialog() && WizardManagerHelper.isAnySetupWizard(getIntent())) {
306             BiometricsSplitScreenDialog.newInstance(getModality(), false /*destroyActivity*/)
307                     .show(getSupportFragmentManager(), BiometricsSplitScreenDialog.class.getName());
308             return;
309         }
310 
311         mNextClicked = true;
312         if (checkMaxEnrolled() == 0) {
313             // Lock thingy is already set up, launch directly to the next page
314             launchNextEnrollingActivity(mToken);
315         } else {
316             boolean couldStartNextBiometric = BiometricUtils.tryStartingNextBiometricEnroll(this,
317                     ENROLL_NEXT_BIOMETRIC_REQUEST, "enrollIntroduction#onNextButtonClicked");
318             if (!couldStartNextBiometric) {
319                 setResult(RESULT_FINISHED);
320                 finish();
321             }
322         }
323         mNextLaunched = true;
324     }
325 
launchChooseLock()326     private void launchChooseLock() {
327         Intent intent = BiometricUtils.getChooseLockIntent(this, getIntent());
328         intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true);
329         intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true);
330         intent.putExtra(getExtraKeyForBiometric(), true);
331         if (mUserId != UserHandle.USER_NULL) {
332             intent.putExtra(Intent.EXTRA_USER_ID, mUserId);
333         }
334         startActivityForResult(intent, CHOOSE_LOCK_GENERIC_REQUEST);
335     }
336 
launchNextEnrollingActivity(byte[] token)337     private void launchNextEnrollingActivity(byte[] token) {
338         Intent intent = getEnrollingIntent();
339         if (token != null) {
340             intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token);
341         }
342         if (mUserId != UserHandle.USER_NULL) {
343             intent.putExtra(Intent.EXTRA_USER_ID, mUserId);
344         }
345         BiometricUtils.copyMultiBiometricExtras(getIntent(), intent);
346         intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, mFromSettingsSummary);
347         intent.putExtra(EXTRA_KEY_CHALLENGE, mChallenge);
348         intent.putExtra(EXTRA_KEY_SENSOR_ID, mSensorId);
349         startActivityForResult(intent, BIOMETRIC_FIND_SENSOR_REQUEST);
350     }
351 
352     /**
353      * Returns the intent extra data for setResult(), null means nothing need to been sent back
354      */
355     @Nullable
getSetResultIntentExtra(@ullable Intent activityResultIntent)356     protected Intent getSetResultIntentExtra(@Nullable Intent activityResultIntent) {
357         return activityResultIntent;
358     }
359 
360     @Override
onActivityResult(int requestCode, int resultCode, Intent data)361     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
362         Log.d(TAG,
363                 "onActivityResult(requestCode=" + requestCode + ", resultCode=" + resultCode + ")");
364         final boolean cameFromMultiBioFpAuthAddAnother =
365                 requestCode == BiometricUtils.REQUEST_ADD_ANOTHER
366                 && BiometricUtils.isMultiBiometricFingerprintEnrollmentFlow(this);
367         if (requestCode == BIOMETRIC_FIND_SENSOR_REQUEST) {
368             if (isResultFinished(resultCode)) {
369                 handleBiometricResultSkipOrFinished(resultCode, getSetResultIntentExtra(data));
370             } else if (isResultSkipped(resultCode)) {
371                 if (!BiometricUtils.tryStartingNextBiometricEnroll(this,
372                         ENROLL_NEXT_BIOMETRIC_REQUEST, "BIOMETRIC_FIND_SENSOR_SKIPPED")) {
373                     handleBiometricResultSkipOrFinished(resultCode, data);
374                 }
375             } else if (resultCode == RESULT_TIMEOUT) {
376                 setResult(resultCode, data);
377                 finish();
378             }
379         } else if (requestCode == CHOOSE_LOCK_GENERIC_REQUEST) {
380             mConfirmingCredentials = false;
381             if (resultCode == RESULT_FINISHED) {
382                 updatePasswordQuality();
383                 final boolean handled = onSetOrConfirmCredentials(data);
384                 if (!handled) {
385                     overridePendingTransition(
386                             com.google.android.setupdesign.R.anim.sud_slide_next_in,
387                             com.google.android.setupdesign.R.anim.sud_slide_next_out);
388                     getNextButton().setEnabled(false);
389                     getChallenge(((sensorId, userId, challenge) -> {
390                         mSensorId = sensorId;
391                         mChallenge = challenge;
392                         mToken = BiometricUtils.requestGatekeeperHat(this, data, mUserId,
393                                 challenge);
394                         BiometricUtils.removeGatekeeperPasswordHandle(this, data);
395                         getNextButton().setEnabled(true);
396                     }));
397                 }
398             } else {
399                 setResult(resultCode, data);
400                 finish();
401             }
402         } else if (requestCode == CONFIRM_REQUEST) {
403             mConfirmingCredentials = false;
404             if (resultCode == RESULT_OK && data != null) {
405                 final boolean handled = onSetOrConfirmCredentials(data);
406                 if (!handled) {
407                     overridePendingTransition(
408                             com.google.android.setupdesign.R.anim.sud_slide_next_in,
409                             com.google.android.setupdesign.R.anim.sud_slide_next_out);
410                     getNextButton().setEnabled(false);
411                     getChallenge(((sensorId, userId, challenge) -> {
412                         mSensorId = sensorId;
413                         mChallenge = challenge;
414                         mToken = BiometricUtils.requestGatekeeperHat(this, data, mUserId,
415                                 challenge);
416                         BiometricUtils.removeGatekeeperPasswordHandle(this, data);
417                         getNextButton().setEnabled(true);
418                     }));
419                 }
420             } else {
421                 setResult(resultCode, data);
422                 finish();
423             }
424         } else if (requestCode == LEARN_MORE_REQUEST) {
425             overridePendingTransition(
426                     com.google.android.setupdesign.R.anim.sud_slide_back_in,
427                     com.google.android.setupdesign.R.anim.sud_slide_back_out);
428         } else if (requestCode == ENROLL_NEXT_BIOMETRIC_REQUEST
429                 || cameFromMultiBioFpAuthAddAnother) {
430             if (isResultFinished(resultCode)) {
431                 handleBiometricResultSkipOrFinished(resultCode, data);
432             } else if (isResultSkipped(resultCode)) {
433                 if (requestCode == BiometricUtils.REQUEST_ADD_ANOTHER) {
434                     // If we came from an add another request, it still might
435                     // be possible to add another biometric. Check if we can.
436                     if (checkMaxEnrolled() != 0) {
437                         // If we can't enroll any more biometrics, than skip
438                         // this one.
439                         handleBiometricResultSkipOrFinished(resultCode, data);
440                     }
441                 } else {
442                     handleBiometricResultSkipOrFinished(resultCode, data);
443                 }
444             } else if (resultCode != RESULT_CANCELED) {
445                 setResult(resultCode, data);
446                 finish();
447             }
448         }
449         super.onActivityResult(requestCode, resultCode, data);
450     }
451 
isResultSkipped(int resultCode)452     private static boolean isResultSkipped(int resultCode) {
453         return resultCode == RESULT_SKIP
454                 || resultCode == SetupSkipDialog.RESULT_SKIP;
455     }
456 
isResultFinished(int resultCode)457     private static boolean isResultFinished(int resultCode) {
458         return resultCode == RESULT_FINISHED;
459     }
460 
isResultSkipOrFinished(int resultCode)461     private static boolean isResultSkipOrFinished(int resultCode) {
462         return isResultSkipped(resultCode) || isResultFinished(resultCode);
463     }
464 
removeEnrollNextBiometric()465     protected void removeEnrollNextBiometric() {
466         getIntent().removeExtra(MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FACE);
467         getIntent().removeExtra(MultiBiometricEnrollHelper.EXTRA_ENROLL_AFTER_FINGERPRINT);
468     }
469 
removeEnrollNextBiometricIfSkipEnroll(@ullable Intent data)470     protected void removeEnrollNextBiometricIfSkipEnroll(@Nullable Intent data) {
471         if (data != null
472                 && data.getBooleanExtra(
473                         MultiBiometricEnrollHelper.EXTRA_SKIP_PENDING_ENROLL, false)) {
474             removeEnrollNextBiometric();
475         }
476     }
handleBiometricResultSkipOrFinished(int resultCode, @Nullable Intent data)477     protected void handleBiometricResultSkipOrFinished(int resultCode, @Nullable Intent data) {
478         removeEnrollNextBiometricIfSkipEnroll(data);
479         if (resultCode == RESULT_SKIP) {
480             onEnrollmentSkipped(data);
481         } else if (resultCode == RESULT_FINISHED) {
482             onFinishedEnrolling(data);
483         }
484     }
485 
486     /**
487      * Called after confirming credentials. Can be used to prevent the default
488      * behavior of immediately calling #getChallenge (useful to things like intro
489      * consent screens that don't actually do enrollment and will later start an
490      * activity that does).
491      *
492      * @return True if the default behavior should be skipped and handled by this method instead.
493      */
onSetOrConfirmCredentials(@ullable Intent data)494     protected boolean onSetOrConfirmCredentials(@Nullable Intent data) {
495         return false;
496     }
497 
onCancelButtonClick(View view)498     protected void onCancelButtonClick(View view) {
499         finish();
500     }
501 
onSkipButtonClick(View view)502     protected void onSkipButtonClick(View view) {
503         onEnrollmentSkipped(null /* data */);
504     }
505 
onEnrollmentSkipped(@ullable Intent data)506     protected void onEnrollmentSkipped(@Nullable Intent data) {
507         setResult(RESULT_SKIP, data);
508         finish();
509     }
510 
onFinishedEnrolling(@ullable Intent data)511     protected void onFinishedEnrolling(@Nullable Intent data) {
512         setResult(RESULT_FINISHED, data);
513         finish();
514     }
515 
updateDescriptionText()516     protected void updateDescriptionText() {
517         if (mBiometricUnlockDisabledByAdmin && !mParentalConsentRequired) {
518             setDescriptionText(getDescriptionDisabledByAdmin());
519         }
520     }
521 
522     @Override
initViews()523     protected void initViews() {
524         super.initViews();
525         updateDescriptionText();
526     }
527 
528     @NonNull
getIconColorFilter()529     protected PorterDuffColorFilter getIconColorFilter() {
530         if (mIconColorFilter == null) {
531             mIconColorFilter = new PorterDuffColorFilter(
532                     DynamicColorPalette.getColor(this, DynamicColorPalette.ColorType.ACCENT),
533                     PorterDuff.Mode.SRC_IN);
534         }
535         return mIconColorFilter;
536     }
537 
538     @NonNull
getPrimaryFooterButton()539     protected abstract FooterButton getPrimaryFooterButton();
540 
541     @NonNull
getSecondaryFooterButton()542     protected abstract FooterButton getSecondaryFooterButton();
543 
544     @StringRes
getAgreeButtonTextRes()545     protected abstract int getAgreeButtonTextRes();
546 
547     @StringRes
getMoreButtonTextRes()548     protected abstract int getMoreButtonTextRes();
549 }
550