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 package com.android.settings.biometrics.combination; 17 18 import static android.app.Activity.RESULT_OK; 19 20 import static com.android.settings.password.ChooseLockPattern.RESULT_FINISHED; 21 22 import android.app.Activity; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.hardware.biometrics.SensorProperties; 26 import android.hardware.face.FaceManager; 27 import android.hardware.face.FaceSensorPropertiesInternal; 28 import android.hardware.fingerprint.FingerprintManager; 29 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; 30 import android.os.Bundle; 31 import android.os.UserHandle; 32 import android.text.TextUtils; 33 import android.util.Log; 34 35 import androidx.activity.result.ActivityResult; 36 import androidx.activity.result.ActivityResultLauncher; 37 import androidx.activity.result.contract.ActivityResultContracts; 38 import androidx.annotation.NonNull; 39 import androidx.annotation.Nullable; 40 import androidx.annotation.StringRes; 41 import androidx.annotation.VisibleForTesting; 42 import androidx.preference.Preference; 43 44 import com.android.settings.R; 45 import com.android.settings.Utils; 46 import com.android.settings.biometrics.BiometricEnrollBase; 47 import com.android.settings.biometrics.BiometricStatusPreferenceController; 48 import com.android.settings.biometrics.BiometricUtils; 49 import com.android.settings.biometrics.IdentityCheckBiometricErrorDialog; 50 import com.android.settings.core.SettingsBaseActivity; 51 import com.android.settings.dashboard.DashboardFragment; 52 import com.android.settings.password.ChooseLockGeneric; 53 import com.android.settings.password.ChooseLockSettingsHelper; 54 import com.android.settings.password.ConfirmDeviceCredentialActivity; 55 import com.android.settingslib.core.AbstractPreferenceController; 56 import com.android.settingslib.transition.SettingsTransitionHelper; 57 58 import java.util.Collection; 59 import java.util.List; 60 61 /** 62 * Base fragment with the confirming credential functionality for combined biometrics settings. 63 */ 64 public abstract class BiometricsSettingsBase extends DashboardFragment { 65 66 @VisibleForTesting 67 static final int CONFIRM_REQUEST = 2001; 68 private static final int CHOOSE_LOCK_REQUEST = 2002; 69 public static final int ACTIVE_UNLOCK_REQUEST = 2003; 70 @VisibleForTesting 71 static final int BIOMETRIC_AUTH_REQUEST = 2004; 72 73 private static final String SAVE_STATE_CONFIRM_CREDETIAL = "confirm_credential"; 74 private static final String DO_NOT_FINISH_ACTIVITY = "do_not_finish_activity"; 75 @VisibleForTesting 76 static final String RETRY_PREFERENCE_KEY = "retry_preference_key"; 77 @VisibleForTesting 78 static final String RETRY_PREFERENCE_BUNDLE = "retry_preference_bundle"; 79 private static final String BIOMETRICS_AUTH_REQUESTED = "biometrics_auth_requested"; 80 81 protected int mUserId; 82 protected long mGkPwHandle; 83 private boolean mConfirmCredential; 84 private boolean mBiometricsAuthenticationRequested; 85 @Nullable private FaceManager mFaceManager; 86 @Nullable private FingerprintManager mFingerprintManager; 87 // Do not finish() if choosing/confirming credential, showing fp/face settings, or launching 88 // active unlock 89 protected boolean mDoNotFinishActivity; 90 @Nullable private String mRetryPreferenceKey = null; 91 @Nullable private Bundle mRetryPreferenceExtra = null; 92 93 private final ActivityResultLauncher<Intent> mFaceOrFingerprintPreferenceLauncher = 94 registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), 95 this::onFaceOrFingerprintPreferenceResult); 96 onFaceOrFingerprintPreferenceResult(@ullable ActivityResult result)97 private void onFaceOrFingerprintPreferenceResult(@Nullable ActivityResult result) { 98 if (result != null && result.getResultCode() == BiometricEnrollBase.RESULT_TIMEOUT) { 99 // When "Face Unlock" or "Fingerprint Unlock" is closed due to entering onStop(), 100 // "Face & Fingerprint Unlock" shall also close itself and back to "Security" page. 101 finish(); 102 } 103 } 104 105 @Override onAttach(Context context)106 public void onAttach(Context context) { 107 super.onAttach(context); 108 mUserId = getActivity().getIntent().getIntExtra(Intent.EXTRA_USER_ID, 109 UserHandle.myUserId()); 110 } 111 112 @Override onCreate(Bundle savedInstanceState)113 public void onCreate(Bundle savedInstanceState) { 114 super.onCreate(savedInstanceState); 115 mFaceManager = Utils.getFaceManagerOrNull(getActivity()); 116 mFingerprintManager = Utils.getFingerprintManagerOrNull(getActivity()); 117 118 if (BiometricUtils.containsGatekeeperPasswordHandle(getIntent())) { 119 mGkPwHandle = BiometricUtils.getGatekeeperPasswordHandle(getIntent()); 120 } 121 122 if (savedInstanceState != null) { 123 mConfirmCredential = savedInstanceState.getBoolean(SAVE_STATE_CONFIRM_CREDETIAL); 124 mDoNotFinishActivity = savedInstanceState.getBoolean(DO_NOT_FINISH_ACTIVITY); 125 mRetryPreferenceKey = savedInstanceState.getString(RETRY_PREFERENCE_KEY); 126 mRetryPreferenceExtra = savedInstanceState.getBundle(RETRY_PREFERENCE_BUNDLE); 127 if (savedInstanceState.containsKey( 128 ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE)) { 129 mGkPwHandle = savedInstanceState.getLong( 130 ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE); 131 } 132 mBiometricsAuthenticationRequested = savedInstanceState.getBoolean( 133 BIOMETRICS_AUTH_REQUESTED); 134 } 135 136 if (mGkPwHandle == 0L && !mConfirmCredential) { 137 mConfirmCredential = true; 138 launchChooseOrConfirmLock(); 139 } 140 updateUnlockPhonePreferenceSummary(); 141 142 final Preference useInAppsPreference = findPreference(getUseInAppsPreferenceKey()); 143 if (useInAppsPreference != null) { 144 useInAppsPreference.setSummary(getUseClass2BiometricSummary()); 145 } 146 } 147 148 @Override onResume()149 public void onResume() { 150 super.onResume(); 151 if (!mConfirmCredential) { 152 mDoNotFinishActivity = false; 153 } 154 } 155 156 @Override onStop()157 public void onStop() { 158 super.onStop(); 159 if (!getActivity().isChangingConfigurations() && !mDoNotFinishActivity) { 160 BiometricUtils.removeGatekeeperPasswordHandle(getActivity(), mGkPwHandle); 161 getActivity().finish(); 162 } 163 } 164 onRetryPreferenceTreeClick(Preference preference, final boolean retry)165 protected boolean onRetryPreferenceTreeClick(Preference preference, final boolean retry) { 166 final String key = preference.getKey(); 167 final Context context = requireActivity().getApplicationContext(); 168 169 // Generate challenge (and request LSS to create a HAT) each time the preference is clicked, 170 // since FingerprintSettings and FaceSettings revoke the challenge when finishing. 171 if (getFacePreferenceKey().equals(key)) { 172 mDoNotFinishActivity = true; 173 mFaceManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> { 174 final Activity activity = getActivity(); 175 if (activity == null || activity.isFinishing()) { 176 Log.e(getLogTag(), "Stop during generating face unlock challenge" 177 + " because activity is null or finishing"); 178 return; 179 } 180 try { 181 final byte[] token = requestGatekeeperHat(context, mGkPwHandle, mUserId, 182 challenge); 183 final Bundle extras = preference.getExtras(); 184 extras.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); 185 extras.putInt(BiometricEnrollBase.EXTRA_KEY_SENSOR_ID, sensorId); 186 extras.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge); 187 onFaceOrFingerprintPreferenceTreeClick(preference); 188 } catch (IllegalStateException e) { 189 if (retry) { 190 mRetryPreferenceKey = preference.getKey(); 191 mRetryPreferenceExtra = preference.getExtras(); 192 mConfirmCredential = true; 193 launchChooseOrConfirmLock(); 194 } else { 195 Log.e(getLogTag(), "face generateChallenge fail", e); 196 mDoNotFinishActivity = false; 197 } 198 } 199 }); 200 return true; 201 } else if (getFingerprintPreferenceKey().equals(key)) { 202 mDoNotFinishActivity = true; 203 mFingerprintManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> { 204 final Activity activity = getActivity(); 205 if (activity == null || activity.isFinishing()) { 206 Log.e(getLogTag(), "Stop during generating fingerprint challenge" 207 + " because activity is null or finishing"); 208 return; 209 } 210 try { 211 final byte[] token = requestGatekeeperHat(context, mGkPwHandle, mUserId, 212 challenge); 213 final Bundle extras = preference.getExtras(); 214 extras.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); 215 extras.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge); 216 onFaceOrFingerprintPreferenceTreeClick(preference); 217 } catch (IllegalStateException e) { 218 if (retry) { 219 mRetryPreferenceKey = preference.getKey(); 220 mRetryPreferenceExtra = preference.getExtras(); 221 mConfirmCredential = true; 222 launchChooseOrConfirmLock(); 223 } else { 224 Log.e(getLogTag(), "fingerprint generateChallenge fail", e); 225 mDoNotFinishActivity = false; 226 } 227 } 228 }); 229 return true; 230 } 231 return false; 232 } 233 234 @VisibleForTesting requestGatekeeperHat(@onNull Context context, long gkPwHandle, int userId, long challenge)235 protected byte[] requestGatekeeperHat(@NonNull Context context, long gkPwHandle, int userId, 236 long challenge) { 237 return BiometricUtils.requestGatekeeperHat(context, gkPwHandle, userId, challenge); 238 } 239 240 /** 241 * Handle preference tree click action for "Face Unlock" or "Fingerprint Unlock" with a launcher 242 * because "Face & Fingerprint Unlock" has to close itself when it gets a specific activity 243 * error code. 244 * 245 * @param preference "Face Unlock" or "Fingerprint Unlock" preference. 246 */ onFaceOrFingerprintPreferenceTreeClick(@onNull Preference preference)247 private void onFaceOrFingerprintPreferenceTreeClick(@NonNull Preference preference) { 248 Collection<List<AbstractPreferenceController>> controllers = getPreferenceControllers(); 249 for (List<AbstractPreferenceController> controllerList : controllers) { 250 for (AbstractPreferenceController controller : controllerList) { 251 if (controller instanceof BiometricStatusPreferenceController) { 252 final BiometricStatusPreferenceController biometricController = 253 (BiometricStatusPreferenceController) controller; 254 if (biometricController.setPreferenceTreeClickLauncher(preference, 255 mFaceOrFingerprintPreferenceLauncher)) { 256 if (biometricController.handlePreferenceTreeClick(preference)) { 257 writePreferenceClickMetric(preference); 258 } 259 biometricController.setPreferenceTreeClickLauncher(preference, null); 260 return; 261 } 262 } 263 } 264 } 265 } 266 267 @Override onPreferenceTreeClick(Preference preference)268 public boolean onPreferenceTreeClick(Preference preference) { 269 return onRetryPreferenceTreeClick(preference, true) 270 || super.onPreferenceTreeClick(preference); 271 } 272 retryPreferenceKey(@onNull String key, @Nullable Bundle extras)273 private void retryPreferenceKey(@NonNull String key, @Nullable Bundle extras) { 274 final Preference preference = findPreference(key); 275 if (preference == null) { 276 Log.w(getLogTag(), ".retryPreferenceKey, fail to find " + key); 277 return; 278 } 279 280 if (extras != null) { 281 preference.getExtras().putAll(extras); 282 } 283 onRetryPreferenceTreeClick(preference, false); 284 } 285 286 @Override onSaveInstanceState(Bundle outState)287 public void onSaveInstanceState(Bundle outState) { 288 super.onSaveInstanceState(outState); 289 outState.putBoolean(SAVE_STATE_CONFIRM_CREDETIAL, mConfirmCredential); 290 outState.putBoolean(DO_NOT_FINISH_ACTIVITY, mDoNotFinishActivity); 291 if (mGkPwHandle != 0L) { 292 outState.putLong(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, mGkPwHandle); 293 } 294 if (!TextUtils.isEmpty(mRetryPreferenceKey)) { 295 outState.putString(RETRY_PREFERENCE_KEY, mRetryPreferenceKey); 296 outState.putBundle(RETRY_PREFERENCE_BUNDLE, mRetryPreferenceExtra); 297 } 298 outState.putBoolean(BIOMETRICS_AUTH_REQUESTED, 299 mBiometricsAuthenticationRequested); 300 } 301 302 @Override onActivityResult(int requestCode, int resultCode, @Nullable Intent data)303 public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { 304 super.onActivityResult(requestCode, resultCode, data); 305 if (requestCode == CONFIRM_REQUEST || requestCode == CHOOSE_LOCK_REQUEST) { 306 mConfirmCredential = false; 307 mDoNotFinishActivity = false; 308 if (resultCode == RESULT_FINISHED || resultCode == RESULT_OK) { 309 if (BiometricUtils.containsGatekeeperPasswordHandle(data)) { 310 mGkPwHandle = BiometricUtils.getGatekeeperPasswordHandle(data); 311 if (!TextUtils.isEmpty(mRetryPreferenceKey)) { 312 getActivity().overridePendingTransition( 313 com.google.android.setupdesign.R.anim.sud_slide_next_in, 314 com.google.android.setupdesign.R.anim.sud_slide_next_out); 315 retryPreferenceKey(mRetryPreferenceKey, mRetryPreferenceExtra); 316 } 317 final Utils.BiometricStatus biometricAuthStatus = 318 Utils.requestBiometricAuthenticationForMandatoryBiometrics( 319 getActivity(), 320 mBiometricsAuthenticationRequested, 321 mUserId); 322 if (biometricAuthStatus == Utils.BiometricStatus.OK) { 323 mBiometricsAuthenticationRequested = true; 324 Utils.launchBiometricPromptForMandatoryBiometrics(this, 325 BIOMETRIC_AUTH_REQUEST, 326 mUserId, true /* hideBackground */); 327 } else if (biometricAuthStatus != Utils.BiometricStatus.NOT_ACTIVE) { 328 IdentityCheckBiometricErrorDialog 329 .showBiometricErrorDialogAndFinishActivityOnDismiss(getActivity(), 330 biometricAuthStatus); 331 return; 332 } 333 } else { 334 Log.d(getLogTag(), "Data null or GK PW missing."); 335 finish(); 336 } 337 } else { 338 Log.d(getLogTag(), "Password not confirmed."); 339 finish(); 340 } 341 mRetryPreferenceKey = null; 342 mRetryPreferenceExtra = null; 343 } else if (requestCode == BIOMETRIC_AUTH_REQUEST) { 344 mBiometricsAuthenticationRequested = false; 345 if (resultCode != RESULT_OK) { 346 if (resultCode == ConfirmDeviceCredentialActivity.BIOMETRIC_LOCKOUT_ERROR_RESULT) { 347 IdentityCheckBiometricErrorDialog 348 .showBiometricErrorDialogAndFinishActivityOnDismiss(getActivity(), 349 Utils.BiometricStatus.LOCKOUT); 350 } else { 351 finish(); 352 } 353 } 354 } 355 } 356 357 /** 358 * Get the preference key of face for passing through credential data to face settings. 359 */ getFacePreferenceKey()360 public abstract String getFacePreferenceKey(); 361 362 /** 363 * Get the preference key of face for passing through credential data to face settings. 364 */ getFingerprintPreferenceKey()365 public abstract String getFingerprintPreferenceKey(); 366 367 /** 368 * @return The preference key of the "Unlock your phone" setting toggle. 369 */ getUnlockPhonePreferenceKey()370 public abstract String getUnlockPhonePreferenceKey(); 371 372 /** 373 * @return The preference key of the "Verify it's you in apps" setting toggle. 374 */ getUseInAppsPreferenceKey()375 public abstract String getUseInAppsPreferenceKey(); 376 377 @VisibleForTesting launchChooseOrConfirmLock()378 protected void launchChooseOrConfirmLock() { 379 final ChooseLockSettingsHelper.Builder builder = 380 new ChooseLockSettingsHelper.Builder(getActivity(), this) 381 .setRequestCode(CONFIRM_REQUEST) 382 .setTitle(getString(R.string.security_settings_biometric_preference_title)) 383 .setRequestGatekeeperPasswordHandle(true) 384 .setForegroundOnly(true) 385 .setReturnCredentials(true); 386 if (mUserId != UserHandle.USER_NULL) { 387 builder.setUserId(mUserId); 388 } 389 mDoNotFinishActivity = true; 390 final boolean launched = builder.show(); 391 392 if (!launched) { 393 Intent intent = BiometricUtils.getChooseLockIntent(getActivity(), getIntent()); 394 intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, 395 true); 396 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true); 397 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_BIOMETRICS, true); 398 intent.putExtra(SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE, 399 SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE); 400 401 if (mUserId != UserHandle.USER_NULL) { 402 intent.putExtra(Intent.EXTRA_USER_ID, mUserId); 403 } 404 startActivityForResult(intent, CHOOSE_LOCK_REQUEST); 405 } 406 } 407 updateUnlockPhonePreferenceSummary()408 protected void updateUnlockPhonePreferenceSummary() { 409 final Preference unlockPhonePreference = findPreference(getUnlockPhonePreferenceKey()); 410 if (unlockPhonePreference != null) { 411 unlockPhonePreference.setSummary(getUseAnyBiometricSummary()); 412 } 413 } 414 415 @NonNull getUseAnyBiometricSummary()416 protected String getUseAnyBiometricSummary() { 417 boolean isFaceAllowed = mFaceManager != null && mFaceManager.isHardwareDetected(); 418 boolean isFingerprintAllowed = 419 mFingerprintManager != null && mFingerprintManager.isHardwareDetected(); 420 421 @StringRes final int resId = getUseBiometricSummaryRes(isFaceAllowed, isFingerprintAllowed); 422 return resId == 0 ? "" : getString(resId); 423 } 424 getUserId()425 protected int getUserId() { 426 return mUserId; 427 } 428 getGkPwHandle()429 protected long getGkPwHandle() { 430 return mGkPwHandle; 431 } 432 433 @NonNull getUseClass2BiometricSummary()434 private String getUseClass2BiometricSummary() { 435 boolean isFaceAllowed = false; 436 if (mFaceManager != null) { 437 for (final FaceSensorPropertiesInternal sensorProps 438 : mFaceManager.getSensorPropertiesInternal()) { 439 if (sensorProps.sensorStrength == SensorProperties.STRENGTH_WEAK 440 || sensorProps.sensorStrength == SensorProperties.STRENGTH_STRONG) { 441 isFaceAllowed = true; 442 break; 443 } 444 } 445 } 446 447 boolean isFingerprintAllowed = false; 448 if (mFingerprintManager != null) { 449 for (final FingerprintSensorPropertiesInternal sensorProps 450 : mFingerprintManager.getSensorPropertiesInternal()) { 451 if (sensorProps.sensorStrength == SensorProperties.STRENGTH_WEAK 452 || sensorProps.sensorStrength == SensorProperties.STRENGTH_STRONG) { 453 isFingerprintAllowed = true; 454 break; 455 } 456 } 457 } 458 459 @StringRes final int resId = getUseBiometricSummaryRes(isFaceAllowed, isFingerprintAllowed); 460 return resId == 0 ? "" : getString(resId); 461 } 462 463 @StringRes getUseBiometricSummaryRes(boolean isFaceAllowed, boolean isFingerprintAllowed)464 private static int getUseBiometricSummaryRes(boolean isFaceAllowed, 465 boolean isFingerprintAllowed) { 466 467 if (isFaceAllowed && isFingerprintAllowed) { 468 return R.string.biometric_settings_use_face_or_fingerprint_preference_summary; 469 } else if (isFaceAllowed) { 470 return R.string.biometric_settings_use_face_preference_summary; 471 } else if (isFingerprintAllowed) { 472 return R.string.biometric_settings_use_fingerprint_preference_summary; 473 } else { 474 return 0; 475 } 476 } 477 } 478