1 /* 2 * Copyright (C) 2021 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.face; 18 19 import static android.app.admin.DevicePolicyResources.Strings.Settings.FACE_UNLOCK_DISABLED; 20 21 import static com.android.settings.biometrics.BiometricUtils.GatekeeperCredentialNotMatchException; 22 23 import android.app.admin.DevicePolicyManager; 24 import android.app.settings.SettingsEnums; 25 import android.content.Intent; 26 import android.content.res.Configuration; 27 import android.hardware.SensorPrivacyManager; 28 import android.hardware.biometrics.BiometricAuthenticator; 29 import android.hardware.face.FaceManager; 30 import android.os.Bundle; 31 import android.os.UserHandle; 32 import android.text.Html; 33 import android.text.method.LinkMovementMethod; 34 import android.util.Log; 35 import android.view.View; 36 import android.widget.ImageView; 37 import android.widget.LinearLayout; 38 import android.widget.TextView; 39 40 import androidx.annotation.NonNull; 41 import androidx.annotation.Nullable; 42 import androidx.annotation.StringRes; 43 import androidx.annotation.VisibleForTesting; 44 45 import com.android.settings.R; 46 import com.android.settings.Settings; 47 import com.android.settings.Utils; 48 import com.android.settings.biometrics.BiometricEnrollActivity; 49 import com.android.settings.biometrics.BiometricEnrollIntroduction; 50 import com.android.settings.biometrics.BiometricUtils; 51 import com.android.settings.biometrics.MultiBiometricEnrollHelper; 52 import com.android.settings.password.ChooseLockSettingsHelper; 53 import com.android.settings.password.SetupSkipDialog; 54 import com.android.settings.utils.SensorPrivacyManagerHelper; 55 import com.android.settingslib.RestrictedLockUtilsInternal; 56 import com.android.systemui.unfold.compat.ScreenSizeFoldProvider; 57 import com.android.systemui.unfold.updates.FoldProvider; 58 59 import com.google.android.setupcompat.template.FooterButton; 60 import com.google.android.setupcompat.util.WizardManagerHelper; 61 import com.google.android.setupdesign.span.LinkSpan; 62 63 /** 64 * Provides introductory info about face unlock and prompts the user to agree before starting face 65 * enrollment. 66 */ 67 public class FaceEnrollIntroduction extends BiometricEnrollIntroduction { 68 private static final String TAG = "FaceEnrollIntroduction"; 69 70 private FaceManager mFaceManager; 71 @Nullable private FooterButton mPrimaryFooterButton; 72 @Nullable private FooterButton mSecondaryFooterButton; 73 @Nullable private SensorPrivacyManager mSensorPrivacyManager; 74 75 @Override onCancelButtonClick(View view)76 protected void onCancelButtonClick(View view) { 77 if (!BiometricUtils.tryStartingNextBiometricEnroll(this, ENROLL_NEXT_BIOMETRIC_REQUEST, 78 "cancel")) { 79 super.onCancelButtonClick(view); 80 } 81 } 82 83 @Override onSkipButtonClick(View view)84 protected void onSkipButtonClick(View view) { 85 if (!BiometricUtils.tryStartingNextBiometricEnroll(this, ENROLL_NEXT_BIOMETRIC_REQUEST, 86 "skip")) { 87 super.onSkipButtonClick(view); 88 } 89 } 90 91 @Override onEnrollmentSkipped(@ullable Intent data)92 protected void onEnrollmentSkipped(@Nullable Intent data) { 93 if (!BiometricUtils.tryStartingNextBiometricEnroll(this, ENROLL_NEXT_BIOMETRIC_REQUEST, 94 "skipped")) { 95 super.onEnrollmentSkipped(data); 96 } 97 } 98 99 @Override onFinishedEnrolling(@ullable Intent data)100 protected void onFinishedEnrolling(@Nullable Intent data) { 101 if (!BiometricUtils.tryStartingNextBiometricEnroll(this, ENROLL_NEXT_BIOMETRIC_REQUEST, 102 "finished")) { 103 super.onFinishedEnrolling(data); 104 } 105 } 106 107 @Override shouldFinishWhenBackgrounded()108 protected boolean shouldFinishWhenBackgrounded() { 109 return super.shouldFinishWhenBackgrounded() && !BiometricUtils.isPostureGuidanceShowing( 110 mDevicePostureState, mLaunchedPostureGuidance); 111 } 112 113 @Override onCreate(Bundle savedInstanceState)114 protected void onCreate(Bundle savedInstanceState) { 115 mFaceManager = getFaceManager(); 116 117 if (savedInstanceState == null 118 && !WizardManagerHelper.isAnySetupWizard(getIntent()) 119 && !getIntent().getBooleanExtra(EXTRA_FROM_SETTINGS_SUMMARY, false) 120 && maxFacesEnrolled()) { 121 // from tips && maxEnrolled 122 Log.d(TAG, "launch face settings"); 123 launchFaceSettingsActivity(); 124 finish(); 125 } 126 127 super.onCreate(savedInstanceState); 128 129 // Wait super::onCreated() then return because SuperNotCalledExceptio will be thrown 130 // if we don't wait for it. 131 if (isFinishing()) { 132 return; 133 } 134 135 // Apply extracted theme color to icons. 136 final ImageView iconGlasses = findViewById(R.id.icon_glasses); 137 final ImageView iconLooking = findViewById(R.id.icon_looking); 138 iconGlasses.getBackground().setColorFilter(getIconColorFilter()); 139 iconLooking.getBackground().setColorFilter(getIconColorFilter()); 140 141 // Set text for views with multiple variations. 142 final TextView infoMessageGlasses = findViewById(R.id.info_message_glasses); 143 final TextView infoMessageLooking = findViewById(R.id.info_message_looking); 144 final TextView howMessage = findViewById(R.id.how_message); 145 final TextView inControlTitle = findViewById(R.id.title_in_control); 146 final TextView inControlMessage = findViewById(R.id.message_in_control); 147 final TextView lessSecure = findViewById(R.id.info_message_less_secure); 148 infoMessageGlasses.setText(getInfoMessageGlasses()); 149 infoMessageLooking.setText(getInfoMessageLooking()); 150 inControlTitle.setText(getInControlTitle()); 151 howMessage.setText(getHowMessage()); 152 inControlMessage.setText(Html.fromHtml(getString(getInControlMessage()), 153 Html.FROM_HTML_MODE_LEGACY)); 154 inControlMessage.setMovementMethod(LinkMovementMethod.getInstance()); 155 lessSecure.setText(getLessSecureMessage()); 156 157 // Set up and show the "less secure" info section if necessary. 158 if (getResources().getBoolean(R.bool.config_face_intro_show_less_secure)) { 159 final LinearLayout infoRowLessSecure = findViewById(R.id.info_row_less_secure); 160 final ImageView iconLessSecure = findViewById(R.id.icon_less_secure); 161 infoRowLessSecure.setVisibility(View.VISIBLE); 162 iconLessSecure.getBackground().setColorFilter(getIconColorFilter()); 163 } 164 165 // Set up and show the "require eyes" info section if necessary. 166 if (getResources().getBoolean(R.bool.config_face_intro_show_require_eyes)) { 167 final LinearLayout infoRowRequireEyes = findViewById(R.id.info_row_require_eyes); 168 final ImageView iconRequireEyes = findViewById(R.id.icon_require_eyes); 169 final TextView infoMessageRequireEyes = findViewById(R.id.info_message_require_eyes); 170 infoRowRequireEyes.setVisibility(View.VISIBLE); 171 iconRequireEyes.getBackground().setColorFilter(getIconColorFilter()); 172 infoMessageRequireEyes.setText(getInfoMessageRequireEyes()); 173 } 174 175 // This path is an entry point for SetNewPasswordController, e.g. 176 // adb shell am start -a android.app.action.SET_NEW_PASSWORD 177 if (mToken == null && BiometricUtils.containsGatekeeperPasswordHandle(getIntent())) { 178 if (generateChallengeOnCreate()) { 179 mFooterBarMixin.getPrimaryButton().setEnabled(false); 180 // We either block on generateChallenge, or need to gray out the "next" button until 181 // the challenge is ready. Let's just do this for now. 182 mFaceManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> { 183 if (isFinishing()) { 184 // Do nothing if activity is finishing 185 Log.w(TAG, "activity finished before challenge callback launched."); 186 return; 187 } 188 189 try { 190 mToken = requestGatekeeperHat(challenge); 191 mSensorId = sensorId; 192 mChallenge = challenge; 193 mFooterBarMixin.getPrimaryButton().setEnabled(true); 194 } catch (GatekeeperCredentialNotMatchException e) { 195 // Let BiometricEnrollBase#onCreate() to trigger confirmLock() 196 getIntent().removeExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE); 197 recreate(); 198 } 199 }); 200 } 201 } 202 203 mSensorPrivacyManager = getApplicationContext() 204 .getSystemService(SensorPrivacyManager.class); 205 final SensorPrivacyManagerHelper helper = SensorPrivacyManagerHelper 206 .getInstance(getApplicationContext()); 207 final boolean cameraPrivacyEnabled = helper 208 .isSensorBlocked(SensorPrivacyManager.Sensors.CAMERA, mUserId); 209 Log.v(TAG, "cameraPrivacyEnabled : " + cameraPrivacyEnabled); 210 } 211 launchFaceSettingsActivity()212 private void launchFaceSettingsActivity() { 213 final Intent intent = new Intent(this, Settings.FaceSettingsInternalActivity.class); 214 final byte[] token = getIntent().getByteArrayExtra( 215 ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); 216 if (token != null) { 217 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); 218 } 219 final int userId = getIntent().getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId()); 220 if (userId != UserHandle.USER_NULL) { 221 intent.putExtra(Intent.EXTRA_USER_ID, userId); 222 } 223 BiometricUtils.copyMultiBiometricExtras(getIntent(), intent); 224 intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, true); 225 intent.putExtra(EXTRA_KEY_CHALLENGE, getIntent().getLongExtra(EXTRA_KEY_CHALLENGE, -1L)); 226 intent.putExtra(EXTRA_KEY_SENSOR_ID, getIntent().getIntExtra(EXTRA_KEY_SENSOR_ID, -1)); 227 startActivity(intent); 228 } 229 230 @VisibleForTesting 231 @Nullable getFaceManager()232 protected FaceManager getFaceManager() { 233 return Utils.getFaceManagerOrNull(this); 234 } 235 236 @VisibleForTesting 237 @Nullable getPostureGuidanceIntent()238 protected Intent getPostureGuidanceIntent() { 239 return mPostureGuidanceIntent; 240 } 241 242 @VisibleForTesting 243 @Nullable getPostureCallback()244 protected FoldProvider.FoldCallback getPostureCallback() { 245 return mFoldCallback; 246 } 247 248 @VisibleForTesting 249 @BiometricUtils.DevicePostureInt getDevicePostureState()250 protected int getDevicePostureState() { 251 return mDevicePostureState; 252 } 253 254 @VisibleForTesting 255 @Nullable requestGatekeeperHat(long challenge)256 protected byte[] requestGatekeeperHat(long challenge) { 257 return BiometricUtils.requestGatekeeperHat(this, getIntent(), mUserId, challenge); 258 } 259 260 @Override onConfigurationChanged(@onNull Configuration newConfig)261 public void onConfigurationChanged(@NonNull Configuration newConfig) { 262 super.onConfigurationChanged(newConfig); 263 if (mScreenSizeFoldProvider != null && getPostureCallback() != null) { 264 mScreenSizeFoldProvider.onConfigurationChange(newConfig); 265 } 266 } 267 268 @Override onStart()269 protected void onStart() { 270 super.onStart(); 271 listenFoldEventForPostureGuidance(); 272 } 273 listenFoldEventForPostureGuidance()274 private void listenFoldEventForPostureGuidance() { 275 if (maxFacesEnrolled()) { 276 Log.d(TAG, "Device has enrolled face, do not show posture guidance"); 277 return; 278 } 279 280 if (getPostureGuidanceIntent() == null) { 281 Log.d(TAG, "Device do not support posture guidance"); 282 return; 283 } 284 285 BiometricUtils.setDevicePosturesAllowEnroll( 286 getResources().getInteger(R.integer.config_face_enroll_supported_posture)); 287 288 if (getPostureCallback() == null) { 289 mFoldCallback = isFolded -> { 290 mDevicePostureState = isFolded ? BiometricUtils.DEVICE_POSTURE_CLOSED 291 : BiometricUtils.DEVICE_POSTURE_OPENED; 292 if (BiometricUtils.shouldShowPostureGuidance(mDevicePostureState, 293 mLaunchedPostureGuidance) && !mNextLaunched) { 294 launchPostureGuidance(); 295 } 296 }; 297 } 298 299 if (mScreenSizeFoldProvider == null) { 300 mScreenSizeFoldProvider = new ScreenSizeFoldProvider(getApplicationContext()); 301 mScreenSizeFoldProvider.registerCallback(mFoldCallback, getMainExecutor()); 302 } 303 } 304 305 @Override onActivityResult(int requestCode, int resultCode, Intent data)306 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 307 if (requestCode == REQUEST_POSTURE_GUIDANCE) { 308 mLaunchedPostureGuidance = false; 309 if (resultCode == RESULT_CANCELED || resultCode == RESULT_SKIP) { 310 onSkipButtonClick(getCurrentFocus()); 311 } 312 return; 313 } 314 315 // If user has skipped or finished enrolling, don't restart enrollment. 316 final boolean isEnrollRequest = requestCode == BIOMETRIC_FIND_SENSOR_REQUEST 317 || requestCode == ENROLL_NEXT_BIOMETRIC_REQUEST; 318 final boolean isResultSkipOrFinished = resultCode == RESULT_SKIP 319 || resultCode == SetupSkipDialog.RESULT_SKIP || resultCode == RESULT_FINISHED; 320 boolean hasEnrolledFace = false; 321 if (data != null) { 322 hasEnrolledFace = data.getBooleanExtra(EXTRA_FINISHED_ENROLL_FACE, false); 323 } 324 325 if (resultCode == RESULT_CANCELED) { 326 if (hasEnrolledFace || !BiometricUtils.isPostureAllowEnrollment(mDevicePostureState)) { 327 setResult(resultCode, data); 328 finish(); 329 return; 330 } 331 } 332 333 if (isEnrollRequest && isResultSkipOrFinished || hasEnrolledFace) { 334 data = setSkipPendingEnroll(data); 335 } 336 super.onActivityResult(requestCode, resultCode, data); 337 } 338 generateChallengeOnCreate()339 protected boolean generateChallengeOnCreate() { 340 return true; 341 } 342 343 @StringRes getInfoMessageGlasses()344 protected int getInfoMessageGlasses() { 345 return R.string.security_settings_face_enroll_introduction_info_glasses; 346 } 347 348 @StringRes getInfoMessageLooking()349 protected int getInfoMessageLooking() { 350 return R.string.security_settings_face_enroll_introduction_info_looking; 351 } 352 353 @StringRes getInfoMessageRequireEyes()354 protected int getInfoMessageRequireEyes() { 355 return R.string.security_settings_face_enroll_introduction_info_gaze; 356 } 357 358 @StringRes getHowMessage()359 protected int getHowMessage() { 360 return R.string.security_settings_face_enroll_introduction_how_message; 361 } 362 363 @StringRes getInControlTitle()364 protected int getInControlTitle() { 365 return R.string.security_settings_face_enroll_introduction_control_title; 366 } 367 368 @StringRes getInControlMessage()369 protected int getInControlMessage() { 370 return R.string.security_settings_face_enroll_introduction_control_message; 371 } 372 373 @StringRes getLessSecureMessage()374 protected int getLessSecureMessage() { 375 return R.string.security_settings_face_enroll_introduction_info_less_secure; 376 } 377 378 @Override isDisabledByAdmin()379 protected boolean isDisabledByAdmin() { 380 return RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( 381 this, DevicePolicyManager.KEYGUARD_DISABLE_FACE, mUserId) != null; 382 } 383 384 @Override getLayoutResource()385 protected int getLayoutResource() { 386 return R.layout.face_enroll_introduction; 387 } 388 389 @Override getHeaderResDisabledByAdmin()390 protected int getHeaderResDisabledByAdmin() { 391 return R.string.security_settings_face_enroll_introduction_title_unlock_disabled; 392 } 393 394 @Override getHeaderResDefault()395 protected int getHeaderResDefault() { 396 return R.string.security_settings_face_enroll_introduction_title; 397 } 398 399 @Override getDescriptionDisabledByAdmin()400 protected String getDescriptionDisabledByAdmin() { 401 DevicePolicyManager devicePolicyManager = getSystemService(DevicePolicyManager.class); 402 return devicePolicyManager.getResources().getString( 403 FACE_UNLOCK_DISABLED, 404 () -> getString(R.string.security_settings_face_enroll_introduction_message_unlock_disabled)); 405 } 406 407 @Override getCancelButton()408 protected FooterButton getCancelButton() { 409 if (mFooterBarMixin != null) { 410 return mFooterBarMixin.getSecondaryButton(); 411 } 412 return null; 413 } 414 415 @Override getNextButton()416 protected FooterButton getNextButton() { 417 if (mFooterBarMixin != null) { 418 return mFooterBarMixin.getPrimaryButton(); 419 } 420 return null; 421 } 422 423 @Override getErrorTextView()424 protected TextView getErrorTextView() { 425 return findViewById(R.id.error_text); 426 } 427 maxFacesEnrolled()428 private boolean maxFacesEnrolled() { 429 if (mFaceManager != null) { 430 // This will need to be updated for devices with multiple face sensors. 431 final int numEnrolledFaces = mFaceManager.getEnrolledFaces(mUserId).size(); 432 final int maxFacesEnrollable = getApplicationContext().getResources() 433 .getInteger(R.integer.suw_max_faces_enrollable); 434 return numEnrolledFaces >= maxFacesEnrollable; 435 } else { 436 return false; 437 } 438 } 439 440 //TODO: Refactor this to something that conveys it is used for getting a string ID. 441 @Override checkMaxEnrolled()442 protected int checkMaxEnrolled() { 443 if (mFaceManager != null) { 444 if (maxFacesEnrolled()) { 445 return R.string.face_intro_error_max; 446 } 447 } else { 448 return R.string.face_intro_error_unknown; 449 } 450 return 0; 451 } 452 453 @Override getChallenge(GenerateChallengeCallback callback)454 protected void getChallenge(GenerateChallengeCallback callback) { 455 mFaceManager = Utils.getFaceManagerOrNull(this); 456 if (mFaceManager == null) { 457 callback.onChallengeGenerated(0, 0, 0L); 458 return; 459 } 460 mFaceManager.generateChallenge(mUserId, callback::onChallengeGenerated); 461 } 462 463 @Override getExtraKeyForBiometric()464 protected String getExtraKeyForBiometric() { 465 return ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE; 466 } 467 468 @Override getEnrollingIntent()469 protected Intent getEnrollingIntent() { 470 Intent intent = new Intent(this, FaceEnrollEducation.class); 471 WizardManagerHelper.copyWizardManagerExtras(getIntent(), intent); 472 return intent; 473 } 474 475 @Override getConfirmLockTitleResId()476 protected int getConfirmLockTitleResId() { 477 return R.string.security_settings_face_preference_title; 478 } 479 480 @Override getMetricsCategory()481 public int getMetricsCategory() { 482 return SettingsEnums.FACE_ENROLL_INTRO; 483 } 484 485 @Override onClick(LinkSpan span)486 public void onClick(LinkSpan span) { 487 // TODO(b/110906762) 488 } 489 490 @Override getModality()491 public @BiometricAuthenticator.Modality int getModality() { 492 return BiometricAuthenticator.TYPE_FACE; 493 } 494 495 @Override onNextButtonClick(View view)496 protected void onNextButtonClick(View view) { 497 final boolean parentelConsentRequired = 498 getIntent() 499 .getBooleanExtra(BiometricEnrollActivity.EXTRA_REQUIRE_PARENTAL_CONSENT, false); 500 final boolean cameraPrivacyEnabled = SensorPrivacyManagerHelper 501 .getInstance(getApplicationContext()) 502 .isSensorBlocked(SensorPrivacyManager.Sensors.CAMERA, mUserId); 503 final boolean isSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent()); 504 final boolean isSettingUp = isSetupWizard || (parentelConsentRequired 505 && !WizardManagerHelper.isUserSetupComplete(this)); 506 if (cameraPrivacyEnabled && !isSettingUp) { 507 if (mSensorPrivacyManager == null) { 508 mSensorPrivacyManager = getApplicationContext() 509 .getSystemService(SensorPrivacyManager.class); 510 } 511 mSensorPrivacyManager.showSensorUseDialog(SensorPrivacyManager.Sensors.CAMERA); 512 } else { 513 super.onNextButtonClick(view); 514 } 515 } 516 517 @Override 518 @NonNull getPrimaryFooterButton()519 protected FooterButton getPrimaryFooterButton() { 520 if (mPrimaryFooterButton == null) { 521 mPrimaryFooterButton = new FooterButton.Builder(this) 522 .setText(R.string.security_settings_face_enroll_introduction_agree) 523 .setButtonType(FooterButton.ButtonType.OPT_IN) 524 .setListener(this::onNextButtonClick) 525 .setTheme(R.style.SudGlifButton_Primary) 526 .build(); 527 } 528 return mPrimaryFooterButton; 529 } 530 531 @Override 532 @NonNull getSecondaryFooterButton()533 protected FooterButton getSecondaryFooterButton() { 534 if (mSecondaryFooterButton == null) { 535 mSecondaryFooterButton = new FooterButton.Builder(this) 536 .setText(R.string.security_settings_face_enroll_introduction_no_thanks) 537 .setListener(this::onSkipButtonClick) 538 .setButtonType(FooterButton.ButtonType.NEXT) 539 .setTheme(R.style.SudGlifButton_Primary) 540 .build(); 541 } 542 return mSecondaryFooterButton; 543 } 544 545 @Override 546 @StringRes getAgreeButtonTextRes()547 protected int getAgreeButtonTextRes() { 548 return R.string.security_settings_fingerprint_enroll_introduction_agree; 549 } 550 551 @Override 552 @StringRes getMoreButtonTextRes()553 protected int getMoreButtonTextRes() { 554 return R.string.security_settings_face_enroll_introduction_more; 555 } 556 557 @NonNull setSkipPendingEnroll(@ullable Intent data)558 protected static Intent setSkipPendingEnroll(@Nullable Intent data) { 559 if (data == null) { 560 data = new Intent(); 561 } 562 data.putExtra(MultiBiometricEnrollHelper.EXTRA_SKIP_PENDING_ENROLL, true); 563 return data; 564 } 565 } 566