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