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 20 import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; 21 22 import android.app.Activity; 23 import android.app.Dialog; 24 import android.app.admin.DevicePolicyManager; 25 import android.app.settings.SettingsEnums; 26 import android.content.Context; 27 import android.content.DialogInterface; 28 import android.content.Intent; 29 import android.graphics.drawable.Drawable; 30 import android.hardware.fingerprint.Fingerprint; 31 import android.hardware.fingerprint.FingerprintManager; 32 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.UserHandle; 36 import android.os.UserManager; 37 import android.text.InputFilter; 38 import android.text.Spanned; 39 import android.text.TextUtils; 40 import android.util.Log; 41 import android.view.View; 42 import android.widget.ImeAwareEditText; 43 import android.widget.Toast; 44 45 import androidx.annotation.NonNull; 46 import androidx.annotation.Nullable; 47 import androidx.annotation.VisibleForTesting; 48 import androidx.appcompat.app.AlertDialog; 49 import androidx.preference.Preference; 50 import androidx.preference.Preference.OnPreferenceChangeListener; 51 import androidx.preference.PreferenceGroup; 52 import androidx.preference.PreferenceScreen; 53 import androidx.preference.PreferenceViewHolder; 54 55 import com.android.settings.R; 56 import com.android.settings.SettingsPreferenceFragment; 57 import com.android.settings.SubSettings; 58 import com.android.settings.Utils; 59 import com.android.settings.biometrics.BiometricEnrollBase; 60 import com.android.settings.biometrics.BiometricUtils; 61 import com.android.settings.core.instrumentation.InstrumentedDialogFragment; 62 import com.android.settings.password.ChooseLockGeneric; 63 import com.android.settings.password.ChooseLockSettingsHelper; 64 import com.android.settings.utils.AnnotationSpan; 65 import com.android.settingslib.HelpUtils; 66 import com.android.settingslib.RestrictedLockUtils; 67 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; 68 import com.android.settingslib.RestrictedLockUtilsInternal; 69 import com.android.settingslib.widget.FooterPreference; 70 import com.android.settingslib.widget.TwoTargetPreference; 71 72 import java.util.HashMap; 73 import java.util.List; 74 75 /** 76 * Settings screen for fingerprints 77 */ 78 public class FingerprintSettings extends SubSettings { 79 80 private static final String TAG = "FingerprintSettings"; 81 82 private static final long LOCKOUT_DURATION = 30000; // time we have to wait for fp to reset, ms 83 84 public static final String ANNOTATION_URL = "url"; 85 public static final String ANNOTATION_ADMIN_DETAILS = "admin_details"; 86 87 private static final int RESULT_FINISHED = BiometricEnrollBase.RESULT_FINISHED; 88 private static final int RESULT_SKIP = BiometricEnrollBase.RESULT_SKIP; 89 private static final int RESULT_TIMEOUT = BiometricEnrollBase.RESULT_TIMEOUT; 90 91 @Override getIntent()92 public Intent getIntent() { 93 Intent modIntent = new Intent(super.getIntent()); 94 modIntent.putExtra(EXTRA_SHOW_FRAGMENT, FingerprintSettingsFragment.class.getName()); 95 return modIntent; 96 } 97 98 @Override isValidFragment(String fragmentName)99 protected boolean isValidFragment(String fragmentName) { 100 if (FingerprintSettingsFragment.class.getName().equals(fragmentName)) return true; 101 return false; 102 } 103 104 @Override onCreate(Bundle savedInstanceState)105 public void onCreate(Bundle savedInstanceState) { 106 super.onCreate(savedInstanceState); 107 CharSequence msg = getText(R.string.security_settings_fingerprint_preference_title); 108 setTitle(msg); 109 } 110 111 public static class FingerprintSettingsFragment extends SettingsPreferenceFragment 112 implements OnPreferenceChangeListener, FingerprintPreference.OnDeleteClickListener { 113 private static final int RESET_HIGHLIGHT_DELAY_MS = 500; 114 115 private static final String TAG = "FingerprintSettings"; 116 private static final String KEY_FINGERPRINT_ITEM_PREFIX = "key_fingerprint_item"; 117 private static final String KEY_FINGERPRINT_ADD = "key_fingerprint_add"; 118 private static final String KEY_FINGERPRINT_ENABLE_KEYGUARD_TOGGLE = 119 "fingerprint_enable_keyguard_toggle"; 120 private static final String KEY_LAUNCHED_CONFIRM = "launched_confirm"; 121 122 private static final int MSG_REFRESH_FINGERPRINT_TEMPLATES = 1000; 123 private static final int MSG_FINGER_AUTH_SUCCESS = 1001; 124 private static final int MSG_FINGER_AUTH_FAIL = 1002; 125 private static final int MSG_FINGER_AUTH_ERROR = 1003; 126 private static final int MSG_FINGER_AUTH_HELP = 1004; 127 128 private static final int CONFIRM_REQUEST = 101; 129 private static final int CHOOSE_LOCK_GENERIC_REQUEST = 102; 130 131 private static final int ADD_FINGERPRINT_REQUEST = 10; 132 133 protected static final boolean DEBUG = false; 134 135 private FingerprintManager mFingerprintManager; 136 private List<FingerprintSensorPropertiesInternal> mSensorProperties; 137 private boolean mInFingerprintLockout; 138 private byte[] mToken; 139 private boolean mLaunchedConfirm; 140 private Drawable mHighlightDrawable; 141 private int mUserId; 142 private CharSequence mFooterTitle; 143 private boolean mEnrollClicked; 144 145 private long mChallenge; 146 147 private static final String TAG_AUTHENTICATE_SIDECAR = "authenticate_sidecar"; 148 private static final String TAG_REMOVAL_SIDECAR = "removal_sidecar"; 149 private FingerprintAuthenticateSidecar mAuthenticateSidecar; 150 private FingerprintRemoveSidecar mRemovalSidecar; 151 private HashMap<Integer, String> mFingerprintsRenaming; 152 153 FingerprintAuthenticateSidecar.Listener mAuthenticateListener = 154 new FingerprintAuthenticateSidecar.Listener() { 155 @Override 156 public void onAuthenticationSucceeded( 157 FingerprintManager.AuthenticationResult result) { 158 int fingerId = result.getFingerprint().getBiometricId(); 159 mHandler.obtainMessage(MSG_FINGER_AUTH_SUCCESS, fingerId, 0).sendToTarget(); 160 } 161 162 @Override 163 public void onAuthenticationFailed() { 164 mHandler.obtainMessage(MSG_FINGER_AUTH_FAIL).sendToTarget(); 165 } 166 167 @Override 168 public void onAuthenticationError(int errMsgId, CharSequence errString) { 169 mHandler.obtainMessage(MSG_FINGER_AUTH_ERROR, errMsgId, 0, errString) 170 .sendToTarget(); 171 } 172 173 @Override 174 public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { 175 mHandler.obtainMessage(MSG_FINGER_AUTH_HELP, helpMsgId, 0, helpString) 176 .sendToTarget(); 177 } 178 }; 179 180 FingerprintRemoveSidecar.Listener mRemovalListener = 181 new FingerprintRemoveSidecar.Listener() { 182 public void onRemovalSucceeded(Fingerprint fingerprint) { 183 mHandler.obtainMessage(MSG_REFRESH_FINGERPRINT_TEMPLATES, 184 fingerprint.getBiometricId(), 0).sendToTarget(); 185 updateDialog(); 186 } 187 188 public void onRemovalError(Fingerprint fp, int errMsgId, 189 CharSequence errString) { 190 final Activity activity = getActivity(); 191 if (activity != null) { 192 Toast.makeText(activity, errString, Toast.LENGTH_SHORT); 193 } 194 updateDialog(); 195 } 196 197 private void updateDialog() { 198 RenameDialog renameDialog = (RenameDialog) getFragmentManager(). 199 findFragmentByTag(RenameDialog.class.getName()); 200 if (renameDialog != null) { 201 renameDialog.enableDelete(); 202 } 203 } 204 }; 205 206 private final Handler mHandler = new Handler() { 207 @Override 208 public void handleMessage(android.os.Message msg) { 209 switch (msg.what) { 210 case MSG_REFRESH_FINGERPRINT_TEMPLATES: 211 removeFingerprintPreference(msg.arg1); 212 updateAddPreference(); 213 retryFingerprint(); 214 break; 215 case MSG_FINGER_AUTH_SUCCESS: 216 highlightFingerprintItem(msg.arg1); 217 retryFingerprint(); 218 break; 219 case MSG_FINGER_AUTH_FAIL: 220 // No action required... fingerprint will allow up to 5 of these 221 break; 222 case MSG_FINGER_AUTH_ERROR: 223 handleError(msg.arg1 /* errMsgId */, (CharSequence) msg.obj /* errStr */); 224 break; 225 case MSG_FINGER_AUTH_HELP: { 226 // Not used 227 } 228 break; 229 } 230 } 231 }; 232 233 /** 234 * 235 */ handleError(int errMsgId, CharSequence msg)236 protected void handleError(int errMsgId, CharSequence msg) { 237 switch (errMsgId) { 238 case FingerprintManager.FINGERPRINT_ERROR_CANCELED: 239 case FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED: 240 // Only happens if we get preempted by another activity, or canceled by the 241 // user (e.g. swipe up to home). Ignored. 242 return; 243 case FingerprintManager.FINGERPRINT_ERROR_LOCKOUT: 244 mInFingerprintLockout = true; 245 // We've been locked out. Reset after 30s. 246 if (!mHandler.hasCallbacks(mFingerprintLockoutReset)) { 247 mHandler.postDelayed(mFingerprintLockoutReset, 248 LOCKOUT_DURATION); 249 } 250 break; 251 case FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT: 252 mInFingerprintLockout = true; 253 break; 254 } 255 256 if (mInFingerprintLockout) { 257 // Activity can be null on a screen rotation. 258 final Activity activity = getActivity(); 259 if (activity != null) { 260 Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show(); 261 } 262 } 263 retryFingerprint(); // start again 264 } 265 retryFingerprint()266 private void retryFingerprint() { 267 if (isUdfps()) { 268 // Do not authenticate for UDFPS devices. 269 return; 270 } 271 272 if (mRemovalSidecar.inProgress() 273 || 0 == mFingerprintManager.getEnrolledFingerprints(mUserId).size()) { 274 return; 275 } 276 // Don't start authentication if ChooseLockGeneric is showing, otherwise if the user 277 // is in FP lockout, a toast will show on top 278 if (mLaunchedConfirm) { 279 return; 280 } 281 if (!mInFingerprintLockout) { 282 mAuthenticateSidecar.startAuthentication(mUserId); 283 mAuthenticateSidecar.setListener(mAuthenticateListener); 284 } 285 } 286 287 @Override getMetricsCategory()288 public int getMetricsCategory() { 289 return SettingsEnums.FINGERPRINT; 290 } 291 292 @Override onCreate(Bundle savedInstanceState)293 public void onCreate(Bundle savedInstanceState) { 294 super.onCreate(savedInstanceState); 295 296 Activity activity = getActivity(); 297 mFingerprintManager = Utils.getFingerprintManagerOrNull(activity); 298 mSensorProperties = mFingerprintManager.getSensorPropertiesInternal(); 299 300 mToken = getIntent().getByteArrayExtra( 301 ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); 302 mChallenge = activity.getIntent() 303 .getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, -1L); 304 305 mAuthenticateSidecar = (FingerprintAuthenticateSidecar) 306 getFragmentManager().findFragmentByTag(TAG_AUTHENTICATE_SIDECAR); 307 if (mAuthenticateSidecar == null) { 308 mAuthenticateSidecar = new FingerprintAuthenticateSidecar(); 309 getFragmentManager().beginTransaction() 310 .add(mAuthenticateSidecar, TAG_AUTHENTICATE_SIDECAR).commit(); 311 } 312 mAuthenticateSidecar.setFingerprintManager(mFingerprintManager); 313 314 mRemovalSidecar = (FingerprintRemoveSidecar) 315 getFragmentManager().findFragmentByTag(TAG_REMOVAL_SIDECAR); 316 if (mRemovalSidecar == null) { 317 mRemovalSidecar = new FingerprintRemoveSidecar(); 318 getFragmentManager().beginTransaction() 319 .add(mRemovalSidecar, TAG_REMOVAL_SIDECAR).commit(); 320 } 321 mRemovalSidecar.setFingerprintManager(mFingerprintManager); 322 mRemovalSidecar.setListener(mRemovalListener); 323 324 RenameDialog renameDialog = (RenameDialog) getFragmentManager(). 325 findFragmentByTag(RenameDialog.class.getName()); 326 if (renameDialog != null) { 327 renameDialog.setDeleteInProgress(mRemovalSidecar.inProgress()); 328 } 329 330 mFingerprintsRenaming = new HashMap<Integer, String>(); 331 332 if (savedInstanceState != null) { 333 mFingerprintsRenaming = (HashMap<Integer, String>) 334 savedInstanceState.getSerializable("mFingerprintsRenaming"); 335 mToken = savedInstanceState.getByteArray( 336 ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); 337 mLaunchedConfirm = savedInstanceState.getBoolean( 338 KEY_LAUNCHED_CONFIRM, false); 339 } 340 mUserId = getActivity().getIntent().getIntExtra( 341 Intent.EXTRA_USER_ID, UserHandle.myUserId()); 342 343 // Need to authenticate a session token if none 344 if (mToken == null && mLaunchedConfirm == false) { 345 mLaunchedConfirm = true; 346 launchChooseOrConfirmLock(); 347 } 348 349 final EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( 350 activity, DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT, mUserId); 351 final AnnotationSpan.LinkInfo adminLinkInfo = new AnnotationSpan.LinkInfo( 352 ANNOTATION_ADMIN_DETAILS, (view) -> { 353 RestrictedLockUtils.sendShowAdminSupportDetailsIntent(activity, admin); 354 }); 355 final Intent helpIntent = HelpUtils.getHelpIntent( 356 activity, getString(getHelpResource()), activity.getClass().getName()); 357 final AnnotationSpan.LinkInfo linkInfo = new AnnotationSpan.LinkInfo( 358 activity, ANNOTATION_URL, helpIntent); 359 mFooterTitle = AnnotationSpan.linkify(getText(admin != null 360 ? R.string 361 .security_settings_fingerprint_enroll_disclaimer_lockscreen_disabled 362 : R.string.security_settings_fingerprint_v2_home_screen), 363 linkInfo, adminLinkInfo); 364 } 365 isUdfps()366 private boolean isUdfps() { 367 for (FingerprintSensorPropertiesInternal prop : mSensorProperties) { 368 if (prop.isAnyUdfpsType()) { 369 return true; 370 } 371 } 372 return false; 373 } 374 removeFingerprintPreference(int fingerprintId)375 protected void removeFingerprintPreference(int fingerprintId) { 376 String name = genKey(fingerprintId); 377 Preference prefToRemove = findPreference(name); 378 if (prefToRemove != null) { 379 if (!getPreferenceScreen().removePreference(prefToRemove)) { 380 Log.w(TAG, "Failed to remove preference with key " + name); 381 } 382 } else { 383 Log.w(TAG, "Can't find preference to remove: " + name); 384 } 385 } 386 387 /** 388 * Important! 389 * 390 * Don't forget to update the SecuritySearchIndexProvider if you are doing any change in the 391 * logic or adding/removing preferences here. 392 */ createPreferenceHierarchy()393 private PreferenceScreen createPreferenceHierarchy() { 394 PreferenceScreen root = getPreferenceScreen(); 395 if (root != null) { 396 root.removeAll(); 397 } 398 addPreferencesFromResource(R.xml.security_settings_fingerprint); 399 root = getPreferenceScreen(); 400 addFingerprintItemPreferences(root); 401 setPreferenceScreen(root); 402 return root; 403 } 404 addFingerprintItemPreferences(PreferenceGroup root)405 private void addFingerprintItemPreferences(PreferenceGroup root) { 406 root.removeAll(); 407 final List<Fingerprint> items = mFingerprintManager.getEnrolledFingerprints(mUserId); 408 final int fingerprintCount = items.size(); 409 for (int i = 0; i < fingerprintCount; i++) { 410 final Fingerprint item = items.get(i); 411 FingerprintPreference pref = new FingerprintPreference(root.getContext(), 412 this /* onDeleteClickListener */); 413 pref.setKey(genKey(item.getBiometricId())); 414 pref.setTitle(item.getName()); 415 pref.setFingerprint(item); 416 pref.setPersistent(false); 417 pref.setIcon(R.drawable.ic_fingerprint_24dp); 418 if (mRemovalSidecar.isRemovingFingerprint(item.getBiometricId())) { 419 pref.setEnabled(false); 420 } 421 if (mFingerprintsRenaming.containsKey(item.getBiometricId())) { 422 pref.setTitle(mFingerprintsRenaming.get(item.getBiometricId())); 423 } 424 root.addPreference(pref); 425 pref.setOnPreferenceChangeListener(this); 426 } 427 Preference addPreference = new Preference(root.getContext()); 428 addPreference.setKey(KEY_FINGERPRINT_ADD); 429 addPreference.setTitle(R.string.fingerprint_add_title); 430 addPreference.setIcon(R.drawable.ic_add_24dp); 431 root.addPreference(addPreference); 432 addPreference.setOnPreferenceChangeListener(this); 433 updateAddPreference(); 434 createFooterPreference(root); 435 } 436 updateAddPreference()437 private void updateAddPreference() { 438 if (getActivity() == null) return; // Activity went away 439 440 final Preference addPreference = findPreference(KEY_FINGERPRINT_ADD); 441 442 /* Disable preference if too many fingerprints added */ 443 final int max = getContext().getResources().getInteger( 444 com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser); 445 boolean tooMany = mFingerprintManager.getEnrolledFingerprints(mUserId).size() >= max; 446 // retryFingerprint() will be called when remove finishes 447 // need to disable enroll or have a way to determine if enroll is in progress 448 final boolean removalInProgress = mRemovalSidecar.inProgress(); 449 CharSequence maxSummary = tooMany ? 450 getContext().getString(R.string.fingerprint_add_max, max) : ""; 451 addPreference.setSummary(maxSummary); 452 addPreference.setEnabled(!tooMany && !removalInProgress && mToken != null); 453 } 454 createFooterPreference(PreferenceGroup root)455 private void createFooterPreference(PreferenceGroup root) { 456 final Context context = getActivity(); 457 if (context == null) { 458 return; 459 } 460 root.addPreference(new FooterPreference.Builder(context).setTitle( 461 mFooterTitle).build()); 462 } 463 genKey(int id)464 private static String genKey(int id) { 465 return KEY_FINGERPRINT_ITEM_PREFIX + "_" + id; 466 } 467 468 @Override onResume()469 public void onResume() { 470 super.onResume(); 471 mInFingerprintLockout = false; 472 // Make sure we reload the preference hierarchy since fingerprints may be added, 473 // deleted or renamed. 474 updatePreferences(); 475 if (mRemovalSidecar != null) { 476 mRemovalSidecar.setListener(mRemovalListener); 477 } 478 } 479 updatePreferences()480 private void updatePreferences() { 481 createPreferenceHierarchy(); 482 retryFingerprint(); 483 } 484 485 @Override onPause()486 public void onPause() { 487 super.onPause(); 488 if (mRemovalSidecar != null) { 489 mRemovalSidecar.setListener(null); 490 } 491 if (mAuthenticateSidecar != null) { 492 mAuthenticateSidecar.setListener(null); 493 mAuthenticateSidecar.stopAuthentication(); 494 } 495 } 496 497 @Override onStop()498 public void onStop() { 499 super.onStop(); 500 if (!getActivity().isChangingConfigurations() && !mLaunchedConfirm && !mEnrollClicked) { 501 getActivity().finish(); 502 } 503 } 504 505 @Override onSaveInstanceState(final Bundle outState)506 public void onSaveInstanceState(final Bundle outState) { 507 outState.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, 508 mToken); 509 outState.putBoolean(KEY_LAUNCHED_CONFIRM, mLaunchedConfirm); 510 outState.putSerializable("mFingerprintsRenaming", mFingerprintsRenaming); 511 } 512 513 @Override onPreferenceTreeClick(Preference pref)514 public boolean onPreferenceTreeClick(Preference pref) { 515 final String key = pref.getKey(); 516 if (KEY_FINGERPRINT_ADD.equals(key)) { 517 mEnrollClicked = true; 518 Intent intent = new Intent(); 519 intent.setClassName(SETTINGS_PACKAGE_NAME, 520 FingerprintEnrollEnrolling.class.getName()); 521 intent.putExtra(Intent.EXTRA_USER_ID, mUserId); 522 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken); 523 startActivityForResult(intent, ADD_FINGERPRINT_REQUEST); 524 } else if (pref instanceof FingerprintPreference) { 525 FingerprintPreference fpref = (FingerprintPreference) pref; 526 final Fingerprint fp = fpref.getFingerprint(); 527 showRenameDialog(fp); 528 } 529 return super.onPreferenceTreeClick(pref); 530 } 531 532 @Override onDeleteClick(FingerprintPreference p)533 public void onDeleteClick(FingerprintPreference p) { 534 final boolean hasMultipleFingerprint = 535 mFingerprintManager.getEnrolledFingerprints(mUserId).size() > 1; 536 final Fingerprint fp = p.getFingerprint(); 537 538 if (hasMultipleFingerprint) { 539 if (mRemovalSidecar.inProgress()) { 540 Log.d(TAG, "Fingerprint delete in progress, skipping"); 541 return; 542 } 543 DeleteFingerprintDialog.newInstance(fp, this /* target */) 544 .show(getFragmentManager(), DeleteFingerprintDialog.class.getName()); 545 } else { 546 ConfirmLastDeleteDialog lastDeleteDialog = new ConfirmLastDeleteDialog(); 547 final boolean isProfileChallengeUser = 548 UserManager.get(getContext()).isManagedProfile(mUserId); 549 final Bundle args = new Bundle(); 550 args.putParcelable("fingerprint", fp); 551 args.putBoolean("isProfileChallengeUser", isProfileChallengeUser); 552 lastDeleteDialog.setArguments(args); 553 lastDeleteDialog.setTargetFragment(this, 0); 554 lastDeleteDialog.show(getFragmentManager(), 555 ConfirmLastDeleteDialog.class.getName()); 556 } 557 } 558 showRenameDialog(final Fingerprint fp)559 private void showRenameDialog(final Fingerprint fp) { 560 RenameDialog renameDialog = new RenameDialog(); 561 Bundle args = new Bundle(); 562 if (mFingerprintsRenaming.containsKey(fp.getBiometricId())) { 563 final Fingerprint f = new Fingerprint( 564 mFingerprintsRenaming.get(fp.getBiometricId()), 565 fp.getGroupId(), fp.getBiometricId(), fp.getDeviceId()); 566 args.putParcelable("fingerprint", f); 567 } else { 568 args.putParcelable("fingerprint", fp); 569 } 570 renameDialog.setOnDismissListener((dialogInterface) -> { 571 retryFingerprint(); 572 }); 573 renameDialog.setDeleteInProgress(mRemovalSidecar.inProgress()); 574 renameDialog.setArguments(args); 575 renameDialog.setTargetFragment(this, 0); 576 renameDialog.show(getFragmentManager(), RenameDialog.class.getName()); 577 mAuthenticateSidecar.stopAuthentication(); 578 } 579 580 @Override onPreferenceChange(Preference preference, Object value)581 public boolean onPreferenceChange(Preference preference, Object value) { 582 boolean result = true; 583 final String key = preference.getKey(); 584 if (KEY_FINGERPRINT_ENABLE_KEYGUARD_TOGGLE.equals(key)) { 585 // TODO 586 } else { 587 Log.v(TAG, "Unknown key:" + key); 588 } 589 return result; 590 } 591 592 @Override getHelpResource()593 public int getHelpResource() { 594 return R.string.help_url_fingerprint; 595 } 596 597 @Override onActivityResult(int requestCode, int resultCode, Intent data)598 public void onActivityResult(int requestCode, int resultCode, Intent data) { 599 super.onActivityResult(requestCode, resultCode, data); 600 if (requestCode == CONFIRM_REQUEST || requestCode == CHOOSE_LOCK_GENERIC_REQUEST) { 601 mLaunchedConfirm = false; 602 if (resultCode == RESULT_FINISHED || resultCode == RESULT_OK) { 603 if (data != null && BiometricUtils.containsGatekeeperPasswordHandle(data)) { 604 mFingerprintManager.generateChallenge(mUserId, 605 (sensorId, userId, challenge) -> { 606 mToken = BiometricUtils.requestGatekeeperHat(getActivity(), 607 data, 608 mUserId, challenge); 609 mChallenge = challenge; 610 BiometricUtils.removeGatekeeperPasswordHandle(getActivity(), 611 data); 612 updateAddPreference(); 613 }); 614 } else { 615 Log.d(TAG, "Data null or GK PW missing"); 616 finish(); 617 } 618 } else { 619 Log.d(TAG, "Password not confirmed"); 620 finish(); 621 } 622 } else if (requestCode == ADD_FINGERPRINT_REQUEST) { 623 mEnrollClicked = false; 624 if (resultCode == RESULT_TIMEOUT) { 625 Activity activity = getActivity(); 626 activity.setResult(resultCode); 627 activity.finish(); 628 } 629 } 630 } 631 632 @Override onDestroy()633 public void onDestroy() { 634 super.onDestroy(); 635 if (getActivity().isFinishing()) { 636 mFingerprintManager.revokeChallenge(mUserId, mChallenge); 637 } 638 } 639 getHighlightDrawable()640 private Drawable getHighlightDrawable() { 641 if (mHighlightDrawable == null) { 642 final Activity activity = getActivity(); 643 if (activity != null) { 644 mHighlightDrawable = activity.getDrawable(R.drawable.preference_highlight); 645 } 646 } 647 return mHighlightDrawable; 648 } 649 highlightFingerprintItem(int fpId)650 private void highlightFingerprintItem(int fpId) { 651 String prefName = genKey(fpId); 652 FingerprintPreference fpref = (FingerprintPreference) findPreference(prefName); 653 final Drawable highlight = getHighlightDrawable(); 654 if (highlight != null && fpref != null) { 655 final View view = fpref.getView(); 656 if (view == null) { 657 // FingerprintPreference is not bound to UI yet, so view is null. 658 return; 659 } 660 final int centerX = view.getWidth() / 2; 661 final int centerY = view.getHeight() / 2; 662 highlight.setHotspot(centerX, centerY); 663 view.setBackground(highlight); 664 view.setPressed(true); 665 view.setPressed(false); 666 mHandler.postDelayed(new Runnable() { 667 @Override 668 public void run() { 669 view.setBackground(null); 670 } 671 }, RESET_HIGHLIGHT_DELAY_MS); 672 } 673 } 674 launchChooseOrConfirmLock()675 private void launchChooseOrConfirmLock() { 676 final Intent intent = new Intent(); 677 final ChooseLockSettingsHelper.Builder builder = 678 new ChooseLockSettingsHelper.Builder(getActivity(), this); 679 final boolean launched = builder.setRequestCode(CONFIRM_REQUEST) 680 .setTitle(getString(R.string.security_settings_fingerprint_preference_title)) 681 .setRequestGatekeeperPasswordHandle(true) 682 .setUserId(mUserId) 683 .setForegroundOnly(true) 684 .setReturnCredentials(true) 685 .show(); 686 687 if (!launched) { 688 // TODO: This should be cleaned up. ChooseLockGeneric should provide a way of 689 // specifying arguments/requests, instead of relying on callers setting extras. 690 intent.setClassName(SETTINGS_PACKAGE_NAME, ChooseLockGeneric.class.getName()); 691 intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, 692 true); 693 intent.putExtra(Intent.EXTRA_USER_ID, mUserId); 694 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true); 695 intent.putExtra(Intent.EXTRA_USER_ID, mUserId); 696 startActivityForResult(intent, CHOOSE_LOCK_GENERIC_REQUEST); 697 } 698 } 699 700 @VisibleForTesting deleteFingerPrint(Fingerprint fingerPrint)701 void deleteFingerPrint(Fingerprint fingerPrint) { 702 mRemovalSidecar.startRemove(fingerPrint, mUserId); 703 String name = genKey(fingerPrint.getBiometricId()); 704 Preference prefToRemove = findPreference(name); 705 if (prefToRemove != null) { 706 prefToRemove.setEnabled(false); 707 } 708 updateAddPreference(); 709 } 710 renameFingerPrint(int fingerId, String newName)711 private void renameFingerPrint(int fingerId, String newName) { 712 mFingerprintManager.rename(fingerId, mUserId, newName); 713 if (!TextUtils.isEmpty(newName)) { 714 mFingerprintsRenaming.put(fingerId, newName); 715 } 716 updatePreferences(); 717 } 718 719 private final Runnable mFingerprintLockoutReset = new Runnable() { 720 @Override 721 public void run() { 722 mInFingerprintLockout = false; 723 retryFingerprint(); 724 } 725 }; 726 727 public static class DeleteFingerprintDialog extends InstrumentedDialogFragment 728 implements DialogInterface.OnClickListener { 729 730 private static final String KEY_FINGERPRINT = "fingerprint"; 731 private Fingerprint mFp; 732 private AlertDialog mAlertDialog; 733 newInstance(Fingerprint fp, FingerprintSettingsFragment target)734 public static DeleteFingerprintDialog newInstance(Fingerprint fp, 735 FingerprintSettingsFragment target) { 736 final DeleteFingerprintDialog dialog = new DeleteFingerprintDialog(); 737 final Bundle bundle = new Bundle(); 738 bundle.putParcelable(KEY_FINGERPRINT, fp); 739 dialog.setArguments(bundle); 740 dialog.setTargetFragment(target, 0 /* requestCode */); 741 return dialog; 742 } 743 744 @Override getMetricsCategory()745 public int getMetricsCategory() { 746 return SettingsEnums.DIALOG_FINGERPINT_EDIT; 747 } 748 749 @Override onCreateDialog(Bundle savedInstanceState)750 public Dialog onCreateDialog(Bundle savedInstanceState) { 751 mFp = getArguments().getParcelable(KEY_FINGERPRINT); 752 final String title = getString(R.string.fingerprint_delete_title, mFp.getName()); 753 754 mAlertDialog = new AlertDialog.Builder(getActivity()) 755 .setTitle(title) 756 .setMessage(R.string.fingerprint_delete_message) 757 .setPositiveButton( 758 R.string.security_settings_fingerprint_enroll_dialog_delete, 759 this /* onClickListener */) 760 .setNegativeButton(R.string.cancel, null /* onClickListener */) 761 .create(); 762 return mAlertDialog; 763 } 764 765 @Override onClick(DialogInterface dialog, int which)766 public void onClick(DialogInterface dialog, int which) { 767 if (which == DialogInterface.BUTTON_POSITIVE) { 768 final int fingerprintId = mFp.getBiometricId(); 769 Log.v(TAG, "Removing fpId=" + fingerprintId); 770 mMetricsFeatureProvider.action(getContext(), 771 SettingsEnums.ACTION_FINGERPRINT_DELETE, 772 fingerprintId); 773 FingerprintSettingsFragment parent 774 = (FingerprintSettingsFragment) getTargetFragment(); 775 parent.deleteFingerPrint(mFp); 776 } 777 } 778 } 779 getFilters()780 private static InputFilter[] getFilters() { 781 InputFilter filter = new InputFilter() { 782 @Override 783 public CharSequence filter(CharSequence source, int start, int end, 784 Spanned dest, int dstart, int dend) { 785 for (int index = start; index < end; index++) { 786 final char c = source.charAt(index); 787 // KXMLSerializer does not allow these characters, 788 // see KXmlSerializer.java:162. 789 if (c < 0x20) { 790 return ""; 791 } 792 } 793 return null; 794 } 795 }; 796 return new InputFilter[]{filter}; 797 } 798 799 public static class RenameDialog extends InstrumentedDialogFragment { 800 801 private Fingerprint mFp; 802 private ImeAwareEditText mDialogTextField; 803 private AlertDialog mAlertDialog; 804 private @Nullable DialogInterface.OnDismissListener mDismissListener; 805 private boolean mDeleteInProgress; 806 setDeleteInProgress(boolean deleteInProgress)807 public void setDeleteInProgress(boolean deleteInProgress) { 808 mDeleteInProgress = deleteInProgress; 809 } 810 811 @Override onCancel(DialogInterface dialog)812 public void onCancel(DialogInterface dialog) { 813 super.onCancel(dialog); 814 mDismissListener.onDismiss(dialog); 815 } 816 817 @Override onCreateDialog(Bundle savedInstanceState)818 public Dialog onCreateDialog(Bundle savedInstanceState) { 819 mFp = getArguments().getParcelable("fingerprint"); 820 final String fingerName; 821 final int textSelectionStart; 822 final int textSelectionEnd; 823 if (savedInstanceState != null) { 824 fingerName = savedInstanceState.getString("fingerName"); 825 textSelectionStart = savedInstanceState.getInt("startSelection", -1); 826 textSelectionEnd = savedInstanceState.getInt("endSelection", -1); 827 } else { 828 fingerName = null; 829 textSelectionStart = -1; 830 textSelectionEnd = -1; 831 } 832 mAlertDialog = new AlertDialog.Builder(getActivity()) 833 .setView(R.layout.fingerprint_rename_dialog) 834 .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok, 835 new DialogInterface.OnClickListener() { 836 @Override 837 public void onClick(DialogInterface dialog, int which) { 838 final String newName = 839 mDialogTextField.getText().toString(); 840 final CharSequence name = mFp.getName(); 841 if (!TextUtils.equals(newName, name)) { 842 Log.d(TAG, "rename " + name + " to " + newName); 843 mMetricsFeatureProvider.action(getContext(), 844 SettingsEnums.ACTION_FINGERPRINT_RENAME, 845 mFp.getBiometricId()); 846 FingerprintSettingsFragment parent 847 = (FingerprintSettingsFragment) 848 getTargetFragment(); 849 parent.renameFingerPrint(mFp.getBiometricId(), 850 newName); 851 } 852 mDismissListener.onDismiss(dialog); 853 dialog.dismiss(); 854 } 855 }) 856 .create(); 857 mAlertDialog.setOnShowListener(new DialogInterface.OnShowListener() { 858 @Override 859 public void onShow(DialogInterface dialog) { 860 mDialogTextField = mAlertDialog.findViewById(R.id.fingerprint_rename_field); 861 CharSequence name = fingerName == null ? mFp.getName() : fingerName; 862 mDialogTextField.setText(name); 863 mDialogTextField.setFilters(getFilters()); 864 if (textSelectionStart != -1 && textSelectionEnd != -1) { 865 mDialogTextField.setSelection(textSelectionStart, textSelectionEnd); 866 } else { 867 mDialogTextField.selectAll(); 868 } 869 if (mDeleteInProgress) { 870 mAlertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(false); 871 } 872 mDialogTextField.requestFocus(); 873 mDialogTextField.scheduleShowSoftInput(); 874 } 875 }); 876 return mAlertDialog; 877 } 878 setOnDismissListener(@onNull DialogInterface.OnDismissListener listener)879 public void setOnDismissListener(@NonNull DialogInterface.OnDismissListener listener) { 880 mDismissListener = listener; 881 } 882 enableDelete()883 public void enableDelete() { 884 mDeleteInProgress = false; 885 if (mAlertDialog != null) { 886 mAlertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(true); 887 } 888 } 889 890 @Override onSaveInstanceState(Bundle outState)891 public void onSaveInstanceState(Bundle outState) { 892 super.onSaveInstanceState(outState); 893 if (mDialogTextField != null) { 894 outState.putString("fingerName", mDialogTextField.getText().toString()); 895 outState.putInt("startSelection", mDialogTextField.getSelectionStart()); 896 outState.putInt("endSelection", mDialogTextField.getSelectionEnd()); 897 } 898 } 899 900 @Override getMetricsCategory()901 public int getMetricsCategory() { 902 return SettingsEnums.DIALOG_FINGERPINT_EDIT; 903 } 904 } 905 906 public static class ConfirmLastDeleteDialog extends InstrumentedDialogFragment { 907 908 private Fingerprint mFp; 909 910 @Override getMetricsCategory()911 public int getMetricsCategory() { 912 return SettingsEnums.DIALOG_FINGERPINT_DELETE_LAST; 913 } 914 915 @Override onCreateDialog(Bundle savedInstanceState)916 public Dialog onCreateDialog(Bundle savedInstanceState) { 917 mFp = getArguments().getParcelable("fingerprint"); 918 final boolean isProfileChallengeUser = 919 getArguments().getBoolean("isProfileChallengeUser"); 920 final AlertDialog alertDialog = new AlertDialog.Builder(getActivity()) 921 .setTitle(R.string.fingerprint_last_delete_title) 922 .setMessage((isProfileChallengeUser) 923 ? R.string.fingerprint_last_delete_message_profile_challenge 924 : R.string.fingerprint_last_delete_message) 925 .setPositiveButton(R.string.fingerprint_last_delete_confirm, 926 new DialogInterface.OnClickListener() { 927 @Override 928 public void onClick(DialogInterface dialog, int which) { 929 FingerprintSettingsFragment parent 930 = (FingerprintSettingsFragment) getTargetFragment(); 931 parent.deleteFingerPrint(mFp); 932 dialog.dismiss(); 933 } 934 }) 935 .setNegativeButton( 936 R.string.cancel, 937 new DialogInterface.OnClickListener() { 938 @Override 939 public void onClick(DialogInterface dialog, int which) { 940 dialog.dismiss(); 941 } 942 }) 943 .create(); 944 return alertDialog; 945 } 946 } 947 } 948 949 public static class FingerprintPreference extends TwoTargetPreference { 950 951 private final OnDeleteClickListener mOnDeleteClickListener; 952 953 private Fingerprint mFingerprint; 954 private View mView; 955 private View mDeleteView; 956 957 public interface OnDeleteClickListener { onDeleteClick(FingerprintPreference p)958 void onDeleteClick(FingerprintPreference p); 959 } 960 FingerprintPreference(Context context, OnDeleteClickListener onDeleteClickListener)961 public FingerprintPreference(Context context, OnDeleteClickListener onDeleteClickListener) { 962 super(context); 963 mOnDeleteClickListener = onDeleteClickListener; 964 } 965 getView()966 public View getView() { 967 return mView; 968 } 969 setFingerprint(Fingerprint item)970 public void setFingerprint(Fingerprint item) { 971 mFingerprint = item; 972 } 973 getFingerprint()974 public Fingerprint getFingerprint() { 975 return mFingerprint; 976 } 977 978 @Override getSecondTargetResId()979 protected int getSecondTargetResId() { 980 return R.layout.preference_widget_delete; 981 } 982 983 @Override onBindViewHolder(PreferenceViewHolder view)984 public void onBindViewHolder(PreferenceViewHolder view) { 985 super.onBindViewHolder(view); 986 mView = view.itemView; 987 mDeleteView = view.itemView.findViewById(R.id.delete_button); 988 mDeleteView.setOnClickListener(new View.OnClickListener() { 989 @Override 990 public void onClick(View v) { 991 if (mOnDeleteClickListener != null) { 992 mOnDeleteClickListener.onDeleteClick(FingerprintPreference.this); 993 } 994 } 995 }); 996 } 997 } 998 } 999