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, mCanAssumeUdfps); 333 } 334 } 335 336 @Override onStop()337 protected void onStop() { 338 super.onStop(); 339 mScreenSizeFoldProvider.unregisterCallback(this); 340 if (mAnimation != null) { 341 mAnimation.pauseAnimation(); 342 } 343 } 344 345 @Override shouldFinishWhenBackgrounded()346 protected boolean shouldFinishWhenBackgrounded() { 347 return super.shouldFinishWhenBackgrounded() && !mNextClicked; 348 } 349 350 @Override onDestroy()351 protected void onDestroy() { 352 stopListenOrientationEvent(); 353 super.onDestroy(); 354 if (mAnimation != null) { 355 mAnimation.stopAnimation(); 356 } 357 } 358 onStartButtonClick(View view)359 private void onStartButtonClick(View view) { 360 mNextClicked = true; 361 startActivityForResult(getFingerprintEnrollingIntent(), ENROLL_REQUEST); 362 } 363 onSkipButtonClick(View view)364 protected void onSkipButtonClick(View view) { 365 stopLookingForFingerprint(); 366 setResult(RESULT_SKIP); 367 finish(); 368 } 369 proceedToEnrolling(boolean cancelEnrollment)370 private void proceedToEnrolling(boolean cancelEnrollment) { 371 if (mSidecar != null) { 372 if (cancelEnrollment) { 373 if (mSidecar.cancelEnrollment()) { 374 // Enrollment cancel requested. When the cancellation is successful, 375 // onEnrollmentError will be called with FINGERPRINT_ERROR_CANCELED, calling 376 // this again. 377 return; 378 } 379 } 380 mSidecar.setListener(null); 381 getSupportFragmentManager().beginTransaction().remove(mSidecar). 382 commitAllowingStateLoss(); 383 mSidecar = null; 384 startActivityForResult(getFingerprintEnrollingIntent(), ENROLL_REQUEST); 385 } 386 } 387 388 @Override onActivityResult(int requestCode, int resultCode, Intent data)389 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 390 Log.d(TAG, 391 "onActivityResult(requestCode=" + requestCode + ", resultCode=" + resultCode + ")"); 392 boolean enrolledFingerprint = false; 393 if (data != null) { 394 enrolledFingerprint = data.getBooleanExtra(EXTRA_FINISHED_ENROLL_FINGERPRINT, false); 395 } 396 397 if (resultCode == RESULT_CANCELED && enrolledFingerprint) { 398 setResult(resultCode, data); 399 finish(); 400 return; 401 } 402 403 if (requestCode == CONFIRM_REQUEST) { 404 if (resultCode == RESULT_OK && data != null) { 405 throw new IllegalStateException("Pretty sure this is dead code"); 406 /* 407 mToken = data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); 408 overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out); 409 getIntent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken); 410 startLookingForFingerprint(); 411 */ 412 } else { 413 finish(); 414 } 415 } else if (requestCode == ENROLL_REQUEST) { 416 switch (resultCode) { 417 case RESULT_FINISHED: 418 case RESULT_SKIP: 419 case RESULT_TIMEOUT: 420 setResult(resultCode); 421 finish(); 422 break; 423 default: 424 FingerprintManager fpm = Utils.getFingerprintManagerOrNull(this); 425 int enrolled = fpm.getEnrolledFingerprints().size(); 426 final List<FingerprintSensorPropertiesInternal> props = 427 fpm.getSensorPropertiesInternal(); 428 final int maxEnrollments = props.get(0).maxEnrollmentsPerUser; 429 if (enrolled >= maxEnrollments) { 430 finish(); 431 } else { 432 // We came back from enrolling but it wasn't completed, start again. 433 mNextClicked = false; 434 startLookingForFingerprint(); 435 } 436 break; 437 } 438 } else { 439 super.onActivityResult(requestCode, resultCode, data); 440 } 441 } 442 443 @Override getMetricsCategory()444 public int getMetricsCategory() { 445 return SettingsEnums.FINGERPRINT_FIND_SENSOR; 446 } 447 listenOrientationEvent()448 private void listenOrientationEvent() { 449 if (!mCanAssumeSfps) { 450 // Do nothing if the device doesn't support SideFPS. 451 return; 452 } 453 mOrientationEventListener = new OrientationEventListener(this) { 454 @Override 455 public void onOrientationChanged(int orientation) { 456 final int currentRotation = getRotationFromDefault(getDisplay().getRotation()); 457 if ((currentRotation + 2) % 4 == mPreviousRotation) { 458 mPreviousRotation = currentRotation; 459 recreate(); 460 } 461 } 462 }; 463 mOrientationEventListener.enable(); 464 mPreviousRotation = getRotationFromDefault(getDisplay().getRotation()); 465 } 466 stopListenOrientationEvent()467 private void stopListenOrientationEvent() { 468 if (!mCanAssumeSfps) { 469 // Do nothing if the device doesn't support SideFPS. 470 return; 471 } 472 if (mOrientationEventListener != null) { 473 mOrientationEventListener.disable(); 474 } 475 mOrientationEventListener = null; 476 } 477 478 @Override onFoldUpdated(boolean isFolded)479 public void onFoldUpdated(boolean isFolded) { 480 Log.d(TAG, "onFoldUpdated= " + isFolded); 481 mIsFolded = isFolded; 482 } 483 } 484