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