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