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