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