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