1 /* 2 * Copyright (C) 2015 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.fingerprint; 18 19 import static android.text.Layout.HYPHENATION_FREQUENCY_NORMAL; 20 21 import android.app.settings.SettingsEnums; 22 import android.content.Intent; 23 import android.content.res.Configuration; 24 import android.content.res.Resources; 25 import android.hardware.fingerprint.FingerprintManager; 26 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; 27 import android.os.Bundle; 28 import android.util.Log; 29 import android.view.OrientationEventListener; 30 import android.view.Surface; 31 import android.view.View; 32 import android.view.accessibility.AccessibilityManager; 33 34 import androidx.annotation.NonNull; 35 import androidx.annotation.Nullable; 36 37 import com.android.settings.R; 38 import com.android.settings.Utils; 39 import com.android.settings.biometrics.BiometricEnrollBase; 40 import com.android.settings.biometrics.BiometricEnrollSidecar; 41 import com.android.settings.biometrics.BiometricUtils; 42 import com.android.settings.flags.Flags; 43 import com.android.settings.overlay.FeatureFactory; 44 import com.android.settings.password.ChooseLockSettingsHelper; 45 import com.android.settingslib.widget.LottieColorUtils; 46 import com.android.systemui.unfold.compat.ScreenSizeFoldProvider; 47 import com.android.systemui.unfold.updates.FoldProvider; 48 49 import com.airbnb.lottie.LottieAnimationView; 50 import com.google.android.setupcompat.template.FooterBarMixin; 51 import com.google.android.setupcompat.template.FooterButton; 52 53 import java.util.List; 54 55 /** 56 * Activity explaining the fingerprint sensor location for fingerprint enrollment. 57 */ 58 public class FingerprintEnrollFindSensor extends BiometricEnrollBase implements 59 BiometricEnrollSidecar.Listener, FoldProvider.FoldCallback { 60 61 private static final String TAG = "FingerprintEnrollFindSensor"; 62 private static final String SAVED_STATE_IS_NEXT_CLICKED = "is_next_clicked"; 63 64 @Nullable 65 private FingerprintFindSensorAnimation mAnimation; 66 67 @Nullable 68 private LottieAnimationView mIllustrationLottie; 69 70 private FingerprintEnrollSidecar mSidecar; 71 private boolean mNextClicked; 72 private boolean mCanAssumeUdfps; 73 private boolean mCanAssumeSfps; 74 75 private OrientationEventListener mOrientationEventListener; 76 private int mPreviousRotation = 0; 77 private ScreenSizeFoldProvider mScreenSizeFoldProvider; 78 private boolean mIsFolded; 79 private boolean mIsReverseDefaultRotation; 80 @Nullable 81 protected UdfpsEnrollCalibrator mCalibrator; 82 83 @Override onCreate(Bundle savedInstanceState)84 protected void onCreate(Bundle savedInstanceState) { 85 super.onCreate(savedInstanceState); 86 87 final FingerprintManager fingerprintManager = Utils.getFingerprintManagerOrNull(this); 88 final List<FingerprintSensorPropertiesInternal> props = 89 fingerprintManager.getSensorPropertiesInternal(); 90 mCanAssumeUdfps = props != null && props.size() == 1 && props.get(0).isAnyUdfpsType(); 91 mCanAssumeSfps = props != null && props.size() == 1 && props.get(0).isAnySidefpsType(); 92 setContentView(getContentView()); 93 mScreenSizeFoldProvider = new ScreenSizeFoldProvider(getApplicationContext()); 94 mScreenSizeFoldProvider.registerCallback(this, getApplicationContext().getMainExecutor()); 95 mScreenSizeFoldProvider 96 .onConfigurationChange(getApplicationContext().getResources().getConfiguration()); 97 mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class); 98 mFooterBarMixin.setSecondaryButton( 99 new FooterButton.Builder(this) 100 .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip) 101 .setListener(this::onSkipButtonClick) 102 .setButtonType(FooterButton.ButtonType.SKIP) 103 .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary) 104 .build() 105 ); 106 getLayout().getHeaderTextView().setHyphenationFrequency(HYPHENATION_FREQUENCY_NORMAL); 107 108 listenOrientationEvent(); 109 110 if (mCanAssumeUdfps) { 111 setHeaderText(R.string.security_settings_udfps_enroll_find_sensor_title); 112 setDescriptionText(R.string.security_settings_udfps_enroll_find_sensor_message); 113 114 mIllustrationLottie = findViewById(R.id.illustration_lottie); 115 AccessibilityManager am = getSystemService(AccessibilityManager.class); 116 if (am.isEnabled()) { 117 mIllustrationLottie.setAnimation(R.raw.udfps_edu_a11y_lottie); 118 } 119 } else if (mCanAssumeSfps) { 120 setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title); 121 setDescriptionText(R.string.security_settings_sfps_enroll_find_sensor_message); 122 mIsReverseDefaultRotation = getApplicationContext().getResources().getBoolean( 123 com.android.internal.R.bool.config_reverseDefaultRotation); 124 } else { 125 setHeaderText(R.string.security_settings_fingerprint_enroll_find_sensor_title); 126 setDescriptionText(R.string.security_settings_fingerprint_enroll_find_sensor_message); 127 } 128 if (savedInstanceState != null) { 129 mNextClicked = savedInstanceState.getBoolean(SAVED_STATE_IS_NEXT_CLICKED, mNextClicked); 130 } 131 132 // This is an entry point for SetNewPasswordController, e.g. 133 // adb shell am start -a android.app.action.SET_NEW_PASSWORD 134 if (mToken == null && BiometricUtils.containsGatekeeperPasswordHandle(getIntent())) { 135 fingerprintManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> { 136 mChallenge = challenge; 137 mSensorId = sensorId; 138 mToken = BiometricUtils.requestGatekeeperHat(this, getIntent(), mUserId, challenge); 139 140 // Put this into the intent. This is really just to work around the fact that the 141 // enrollment sidecar gets the HAT from the activity's intent, rather than having 142 // it passed in. 143 getIntent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken); 144 145 // Do not start looking for fingerprint if this activity is re-created because it is 146 // waiting for activity result from enrolling activity. 147 if (!mNextClicked) { 148 startLookingForFingerprint(); 149 } 150 }); 151 } else if (mToken != null) { 152 // Do not start looking for fingerprint if this activity is re-created because it is 153 // waiting for activity result from enrolling activity. 154 if (!mNextClicked) { 155 // HAT passed in from somewhere else, such as FingerprintEnrollIntroduction 156 startLookingForFingerprint(); 157 } 158 } else { 159 // There's something wrong with the enrollment flow, this should never happen. 160 throw new IllegalStateException("HAT and GkPwHandle both missing..."); 161 } 162 163 mAnimation = null; 164 if (mCanAssumeUdfps) { 165 if (Flags.udfpsEnrollCalibration()) { 166 mCalibrator = FeatureFactory.getFeatureFactory().getFingerprintFeatureProvider() 167 .getUdfpsEnrollCalibrator(getApplicationContext(), savedInstanceState, 168 getIntent()); 169 if (mCalibrator != null) { 170 mCalibrator.onWaitingPage( 171 getLifecycle(), 172 getSupportFragmentManager(), 173 this::enableUdfpsLottieAndNextButton 174 ); 175 } else { 176 enableUdfpsLottieAndNextButton(); 177 } 178 } else { 179 enableUdfpsLottieAndNextButton(); 180 } 181 } else if (!mCanAssumeSfps) { 182 View animationView = findViewById(R.id.fingerprint_sensor_location_animation); 183 if (animationView instanceof FingerprintFindSensorAnimation) { 184 mAnimation = (FingerprintFindSensorAnimation) animationView; 185 } 186 } 187 } 188 enableUdfpsLottieAndNextButton()189 private void enableUdfpsLottieAndNextButton() { 190 if (isFinishing()) { 191 return; 192 } 193 194 if (mFooterBarMixin.getPrimaryButton() == null) { 195 mFooterBarMixin.setPrimaryButton(new FooterButton.Builder(this) 196 .setText(R.string.security_settings_udfps_enroll_find_sensor_start_button) 197 .setListener(this::onStartButtonClick) 198 .setButtonType(FooterButton.ButtonType.NEXT) 199 .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary) 200 .build() 201 ); 202 } 203 if (mIllustrationLottie != null) { 204 mIllustrationLottie.setOnClickListener(this::onStartButtonClick); 205 } 206 } 207 getRotationFromDefault(int rotation)208 private int getRotationFromDefault(int rotation) { 209 if (mIsReverseDefaultRotation) { 210 return (rotation + 1) % 4; 211 } else { 212 return rotation; 213 } 214 } 215 updateSfpsFindSensorAnimationAsset()216 private void updateSfpsFindSensorAnimationAsset() { 217 mScreenSizeFoldProvider 218 .onConfigurationChange(getApplicationContext().getResources().getConfiguration()); 219 mIllustrationLottie = findViewById(R.id.illustration_lottie); 220 final int rotation = getRotationFromDefault( 221 getApplicationContext().getDisplay().getRotation()); 222 223 switch (rotation) { 224 case Surface.ROTATION_90: 225 if (mIsFolded) { 226 mIllustrationLottie.setAnimation( 227 R.raw.fingerprint_edu_lottie_folded_top_left); 228 } else { 229 mIllustrationLottie.setAnimation( 230 R.raw.fingerprint_edu_lottie_portrait_top_left); 231 } 232 break; 233 case Surface.ROTATION_180: 234 if (mIsFolded) { 235 mIllustrationLottie.setAnimation( 236 R.raw.fingerprint_edu_lottie_folded_bottom_left); 237 } else { 238 mIllustrationLottie.setAnimation( 239 R.raw.fingerprint_edu_lottie_landscape_bottom_left); 240 } 241 break; 242 case Surface.ROTATION_270: 243 if (mIsFolded) { 244 mIllustrationLottie.setAnimation( 245 R.raw.fingerprint_edu_lottie_folded_bottom_right); 246 } else { 247 mIllustrationLottie.setAnimation( 248 R.raw.fingerprint_edu_lottie_portrait_bottom_right); 249 } 250 break; 251 default: 252 if (mIsFolded) { 253 mIllustrationLottie.setAnimation( 254 R.raw.fingerprint_edu_lottie_folded_top_right); 255 } else { 256 mIllustrationLottie.setAnimation( 257 R.raw.fingerprint_edu_lottie_landscape_top_right); 258 } 259 break; 260 } 261 262 LottieColorUtils.applyDynamicColors(getApplicationContext(), mIllustrationLottie); 263 mIllustrationLottie.setVisibility(View.VISIBLE); 264 mIllustrationLottie.playAnimation(); 265 } 266 267 @Override onConfigurationChanged(@onNull Configuration newConfig)268 public void onConfigurationChanged(@NonNull Configuration newConfig) { 269 super.onConfigurationChanged(newConfig); 270 mScreenSizeFoldProvider.onConfigurationChange(newConfig); 271 } 272 273 @Override onResume()274 protected void onResume() { 275 super.onResume(); 276 if (mCanAssumeSfps) { 277 updateSfpsFindSensorAnimationAsset(); 278 } 279 } 280 281 @Override onSaveInstanceState(Bundle outState)282 protected void onSaveInstanceState(Bundle outState) { 283 super.onSaveInstanceState(outState); 284 outState.putBoolean(SAVED_STATE_IS_NEXT_CLICKED, mNextClicked); 285 if (Flags.udfpsEnrollCalibration()) { 286 if (mCalibrator != null) { 287 mCalibrator.onSaveInstanceState(outState); 288 } 289 } 290 } 291 292 @Override getFingerprintEnrollingIntent()293 protected Intent getFingerprintEnrollingIntent() { 294 final Intent ret = super.getFingerprintEnrollingIntent(); 295 ret.putExtra(BiometricUtils.EXTRA_ENROLL_REASON, 296 getIntent().getIntExtra(BiometricUtils.EXTRA_ENROLL_REASON, -1)); 297 if (Flags.udfpsEnrollCalibration()) { 298 if (mCalibrator != null) { 299 ret.putExtras(mCalibrator.getExtrasForNextIntent()); 300 } 301 } 302 return ret; 303 } 304 305 @Override onBackPressed()306 public void onBackPressed() { 307 stopLookingForFingerprint(); 308 super.onBackPressed(); 309 } 310 311 @Override onApplyThemeResource(Resources.Theme theme, int resid, boolean first)312 protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) { 313 theme.applyStyle(R.style.SetupWizardPartnerResource, true); 314 super.onApplyThemeResource(theme, resid, first); 315 } 316 getContentView()317 protected int getContentView() { 318 if (mCanAssumeUdfps) { 319 return R.layout.udfps_enroll_find_sensor_layout; 320 } else if (mCanAssumeSfps) { 321 return R.layout.sfps_enroll_find_sensor_layout; 322 } 323 return R.layout.fingerprint_enroll_find_sensor; 324 } 325 326 @Override onStart()327 protected void onStart() { 328 super.onStart(); 329 if (mAnimation != null) { 330 mAnimation.startAnimation(); 331 } 332 } 333 stopLookingForFingerprint()334 private void stopLookingForFingerprint() { 335 if (mSidecar != null) { 336 mSidecar.setListener(null); 337 mSidecar.cancelEnrollment(); 338 getSupportFragmentManager() 339 .beginTransaction().remove(mSidecar).commitAllowingStateLoss(); 340 mSidecar = null; 341 } 342 } 343 startLookingForFingerprint()344 private void startLookingForFingerprint() { 345 if (mCanAssumeUdfps) { 346 // UDFPS devices use this screen as an educational screen. Users should tap the 347 // "Start" button to move to the next screen to begin enrollment. 348 return; 349 } 350 mSidecar = (FingerprintEnrollSidecar) getSupportFragmentManager().findFragmentByTag( 351 FingerprintEnrollEnrolling.TAG_SIDECAR); 352 if (mSidecar == null) { 353 mSidecar = new FingerprintEnrollSidecar(this, 354 FingerprintManager.ENROLL_FIND_SENSOR, getIntent()); 355 getSupportFragmentManager().beginTransaction() 356 .add(mSidecar, FingerprintEnrollEnrolling.TAG_SIDECAR) 357 .commitAllowingStateLoss(); 358 } 359 mSidecar.setListener(this); 360 } 361 362 @Override onEnrollmentProgressChange(int steps, int remaining)363 public void onEnrollmentProgressChange(int steps, int remaining) { 364 mNextClicked = true; 365 proceedToEnrolling(true /* cancelEnrollment */); 366 } 367 368 @Override onEnrollmentHelp(int helpMsgId, CharSequence helpString)369 public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { 370 } 371 372 @Override onEnrollmentError(int errMsgId, CharSequence errString)373 public void onEnrollmentError(int errMsgId, CharSequence errString) { 374 if (mNextClicked && errMsgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED) { 375 proceedToEnrolling(false /* cancelEnrollment */); 376 } else { 377 FingerprintErrorDialog.showErrorDialog(this, errMsgId, 378 this instanceof SetupFingerprintEnrollFindSensor); 379 } 380 } 381 382 @Override onStop()383 protected void onStop() { 384 super.onStop(); 385 mScreenSizeFoldProvider.unregisterCallback(this); 386 if (mAnimation != null) { 387 mAnimation.pauseAnimation(); 388 } 389 } 390 391 @Override shouldFinishWhenBackgrounded()392 protected boolean shouldFinishWhenBackgrounded() { 393 return super.shouldFinishWhenBackgrounded() && !mNextClicked; 394 } 395 396 @Override onDestroy()397 protected void onDestroy() { 398 stopListenOrientationEvent(); 399 super.onDestroy(); 400 if (mAnimation != null) { 401 mAnimation.stopAnimation(); 402 } 403 } 404 onStartButtonClick(View view)405 private void onStartButtonClick(View view) { 406 mNextClicked = true; 407 startActivityForResult(getFingerprintEnrollingIntent(), ENROLL_REQUEST); 408 } 409 onSkipButtonClick(View view)410 protected void onSkipButtonClick(View view) { 411 stopLookingForFingerprint(); 412 setResult(RESULT_SKIP); 413 finish(); 414 } 415 proceedToEnrolling(boolean cancelEnrollment)416 private void proceedToEnrolling(boolean cancelEnrollment) { 417 if (mSidecar != null) { 418 if (cancelEnrollment) { 419 if (mSidecar.cancelEnrollment()) { 420 // Enrollment cancel requested. When the cancellation is successful, 421 // onEnrollmentError will be called with FINGERPRINT_ERROR_CANCELED, calling 422 // this again. 423 return; 424 } 425 } 426 mSidecar.setListener(null); 427 getSupportFragmentManager().beginTransaction().remove(mSidecar). 428 commitAllowingStateLoss(); 429 mSidecar = null; 430 startActivityForResult(getFingerprintEnrollingIntent(), ENROLL_REQUEST); 431 } 432 } 433 434 @Override onActivityResult(int requestCode, int resultCode, Intent data)435 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 436 Log.d(TAG, 437 "onActivityResult(requestCode=" + requestCode + ", resultCode=" + resultCode + ")"); 438 boolean enrolledFingerprint = false; 439 if (data != null) { 440 enrolledFingerprint = data.getBooleanExtra(EXTRA_FINISHED_ENROLL_FINGERPRINT, false); 441 } 442 443 if (resultCode == RESULT_CANCELED && enrolledFingerprint) { 444 setResult(resultCode, data); 445 finish(); 446 return; 447 } 448 449 if (requestCode == CONFIRM_REQUEST) { 450 if (resultCode == RESULT_OK && data != null) { 451 throw new IllegalStateException("Pretty sure this is dead code"); 452 /* 453 mToken = data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); 454 overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out); 455 getIntent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken); 456 startLookingForFingerprint(); 457 */ 458 } else { 459 finish(); 460 } 461 } else if (requestCode == ENROLL_REQUEST) { 462 switch (resultCode) { 463 case RESULT_FINISHED: 464 case RESULT_SKIP: 465 case RESULT_TIMEOUT: 466 setResult(resultCode); 467 finish(); 468 break; 469 default: 470 FingerprintManager fpm = Utils.getFingerprintManagerOrNull(this); 471 int enrolled = fpm.getEnrolledFingerprints().size(); 472 final List<FingerprintSensorPropertiesInternal> props = 473 fpm.getSensorPropertiesInternal(); 474 final int maxEnrollments = props.get(0).maxEnrollmentsPerUser; 475 if (enrolled >= maxEnrollments) { 476 finish(); 477 } else { 478 // We came back from enrolling but it wasn't completed, start again. 479 mNextClicked = false; 480 startLookingForFingerprint(); 481 } 482 break; 483 } 484 } else { 485 super.onActivityResult(requestCode, resultCode, data); 486 } 487 } 488 489 @Override getMetricsCategory()490 public int getMetricsCategory() { 491 return SettingsEnums.FINGERPRINT_FIND_SENSOR; 492 } 493 listenOrientationEvent()494 private void listenOrientationEvent() { 495 if (!mCanAssumeSfps) { 496 // Do nothing if the device doesn't support SideFPS. 497 return; 498 } 499 mOrientationEventListener = new OrientationEventListener(this) { 500 @Override 501 public void onOrientationChanged(int orientation) { 502 final int currentRotation = getRotationFromDefault(getDisplay().getRotation()); 503 if ((currentRotation + 2) % 4 == mPreviousRotation) { 504 mPreviousRotation = currentRotation; 505 recreate(); 506 } 507 } 508 }; 509 mOrientationEventListener.enable(); 510 mPreviousRotation = getRotationFromDefault(getDisplay().getRotation()); 511 } 512 stopListenOrientationEvent()513 private void stopListenOrientationEvent() { 514 if (!mCanAssumeSfps) { 515 // Do nothing if the device doesn't support SideFPS. 516 return; 517 } 518 if (mOrientationEventListener != null) { 519 mOrientationEventListener.disable(); 520 } 521 mOrientationEventListener = null; 522 } 523 524 @Override onFoldUpdated(boolean isFolded)525 public void onFoldUpdated(boolean isFolded) { 526 Log.d(TAG, "onFoldUpdated= " + isFolded); 527 mIsFolded = isFolded; 528 } 529 } 530