2 * Copyright (C) 2015 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.settings.fingerprint;
20 import android.annotation.Nullable;
21 import android.app.Activity;
22 import android.app.AlertDialog;
23 import android.app.admin.DevicePolicyManager;
24 import android.content.ActivityNotFoundException;
25 import android.content.Context;
26 import android.content.DialogInterface;
27 import android.content.Intent;
28 import android.graphics.Typeface;
29 import android.graphics.drawable.Drawable;
30 import android.hardware.fingerprint.Fingerprint;
31 import android.hardware.fingerprint.FingerprintManager;
32 import android.hardware.fingerprint.FingerprintManager.AuthenticationCallback;
33 import android.hardware.fingerprint.FingerprintManager.AuthenticationResult;
34 import android.hardware.fingerprint.FingerprintManager.RemovalCallback;
35 import android.net.Uri;
36 import android.os.Bundle;
37 import android.os.CancellationSignal;
38 import android.os.Handler;
39 import android.preference.Preference;
40 import android.preference.Preference.OnPreferenceChangeListener;
41 import android.preference.PreferenceGroup;
42 import android.preference.PreferenceScreen;
43 import android.provider.Browser;
44 import android.text.Annotation;
45 import android.text.SpannableString;
46 import android.text.SpannableStringBuilder;
47 import android.text.TextPaint;
48 import android.text.method.LinkMovementMethod;
49 import android.text.style.URLSpan;
50 import android.util.AttributeSet;
51 import android.util.Log;
52 import android.view.LayoutInflater;
53 import android.view.View;
54 import android.view.WindowManager;
55 import android.widget.EditText;
56 import android.widget.TextView;
57 import android.widget.Toast;
59 import com.android.internal.logging.MetricsLogger;
60 import com.android.settings.ChooseLockGeneric;
61 import com.android.settings.ChooseLockSettingsHelper;
62 import com.android.settings.HelpUtils;
63 import com.android.settings.R;
64 import com.android.settings.SettingsPreferenceFragment;
65 import com.android.settings.SubSettings;
66 import com.android.settings.search.Indexable;
68 import java.util.List;
71 * Settings screen for fingerprints
73 public class FingerprintSettings extends SubSettings {
75 * Used by the FP settings wizard to indicate the wizard is
76 * finished, and each activity in the wizard should finish.
78 * Previously, each activity in the wizard would finish itself after
79 * starting the next activity. However, this leads to broken 'Back'
80 * behavior. So, now an activity does not finish itself until it gets this
83 static final int RESULT_FINISHED = RESULT_FIRST_USER;
86 public Intent getIntent() {
87 Intent modIntent = new Intent(super.getIntent());
88 modIntent.putExtra(EXTRA_SHOW_FRAGMENT, FingerprintSettingsFragment.class.getName());
93 protected boolean isValidFragment(String fragmentName) {
94 if (FingerprintSettingsFragment.class.getName().equals(fragmentName)) return true;
99 public void onCreate(Bundle savedInstanceState) {
100 super.onCreate(savedInstanceState);
101 CharSequence msg = getText(R.string.security_settings_fingerprint_preference_title);
105 public static class FingerprintSettingsFragment extends SettingsPreferenceFragment
106 implements OnPreferenceChangeListener, Indexable {
107 private static final int MAX_RETRY_ATTEMPTS = 20;
108 private static final int RESET_HIGHLIGHT_DELAY_MS = 500;
110 private static final String TAG = "FingerprintSettings";
111 private static final String KEY_FINGERPRINT_ITEM_PREFIX = "key_fingerprint_item";
112 private static final String KEY_FINGERPRINT_ADD = "key_fingerprint_add";
113 private static final String KEY_FINGERPRINT_ENABLE_KEYGUARD_TOGGLE =
114 "fingerprint_enable_keyguard_toggle";
115 private static final String KEY_LAUNCHED_CONFIRM = "launched_confirm";
117 private static final int MSG_REFRESH_FINGERPRINT_TEMPLATES = 1000;
118 private static final int MSG_FINGER_AUTH_SUCCESS = 1001;
119 private static final int MSG_FINGER_AUTH_FAIL = 1002;
121 private static final int CONFIRM_REQUEST = 101;
122 private static final int CHOOSE_LOCK_GENERIC_REQUEST = 102;
124 private static final int ADD_FINGERPRINT_REQUEST = 10;
126 protected static final boolean DEBUG = true;
128 private FingerprintManager mFingerprintManager;
129 private EditText mDialogTextField;
130 private PreferenceGroup mManageCategory;
131 private CancellationSignal mFingerprintCancel;
132 private int mMaxFingerprintAttempts;
133 private byte[] mToken;
134 private boolean mLaunchedConfirm;
135 private Drawable mHighlightDrawable;
137 private AuthenticationCallback mAuthCallback = new AuthenticationCallback() {
139 public void onAuthenticationSucceeded(AuthenticationResult result) {
140 int fingerId = result.getFingerprint().getFingerId();
141 mHandler.obtainMessage(MSG_FINGER_AUTH_SUCCESS, fingerId, 0).sendToTarget();
144 public void onAuthenticationFailed() {
145 mHandler.obtainMessage(MSG_FINGER_AUTH_FAIL).sendToTarget();
149 public void onAuthenticationError(int errMsgId, CharSequence errString) {
150 // get activity will be null on a screen rotation
151 Activity activity = getActivity();
152 if (activity != null) {
153 Toast.makeText(activity, errString, Toast.LENGTH_SHORT);
155 if (errMsgId != FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
156 retryFingerprint(false);
161 public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
162 Toast.makeText(getActivity(), helpString, Toast.LENGTH_SHORT);
165 private RemovalCallback mRemoveCallback = new RemovalCallback() {
168 public void onRemovalSucceeded(Fingerprint fingerprint) {
169 mHandler.obtainMessage(MSG_REFRESH_FINGERPRINT_TEMPLATES,
170 fingerprint.getFingerId(), 0).sendToTarget();
174 public void onRemovalError(Fingerprint fp, int errMsgId, CharSequence errString) {
175 Toast.makeText(getActivity(), errString, Toast.LENGTH_SHORT);
178 private final Handler mHandler = new Handler() {
179 public void handleMessage(android.os.Message msg) {
181 case MSG_REFRESH_FINGERPRINT_TEMPLATES:
182 removeFingerprintPreference(msg.arg1);
184 case MSG_FINGER_AUTH_SUCCESS:
185 highlightFingerprintItem(msg.arg1);
186 retryFingerprint(true);
188 case MSG_FINGER_AUTH_FAIL:
189 retryFingerprint(false);
195 private void stopFingerprint() {
196 if (mFingerprintCancel != null) {
197 mFingerprintCancel.cancel();
198 mFingerprintCancel = null;
202 private void retryFingerprint(boolean resetAttempts) {
204 mMaxFingerprintAttempts = 0;
206 if (mMaxFingerprintAttempts < MAX_RETRY_ATTEMPTS) {
207 mFingerprintCancel = new CancellationSignal();
208 mFingerprintManager.authenticate(null, mFingerprintCancel, mAuthCallback, 0);
210 mMaxFingerprintAttempts++;
214 protected int getMetricsCategory() {
215 return MetricsLogger.FINGERPRINT;
219 public void onCreate(Bundle savedInstanceState) {
220 super.onCreate(savedInstanceState);
221 if (savedInstanceState != null) {
222 mToken = savedInstanceState.getByteArray(
223 ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
224 mLaunchedConfirm = savedInstanceState.getBoolean(
225 KEY_LAUNCHED_CONFIRM, false);
228 Activity activity = getActivity();
229 mFingerprintManager = (FingerprintManager) activity.getSystemService(
230 Context.FINGERPRINT_SERVICE);
232 // Need to authenticate a session token if none
233 if (mToken == null && mLaunchedConfirm == false) {
234 mLaunchedConfirm = true;
235 launchChooseOrConfirmLock();
240 public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
241 super.onViewCreated(view, savedInstanceState);
242 TextView v = (TextView) LayoutInflater.from(view.getContext()).inflate(
243 R.layout.fingerprint_settings_footer, null);
244 v.setText(LearnMoreSpan.linkify(getText(isFingerprintDisabled()
245 ? R.string.security_settings_fingerprint_enroll_disclaimer_lockscreen_disabled
246 : R.string.security_settings_fingerprint_enroll_disclaimer),
247 getString(getHelpResource())));
248 v.setMovementMethod(new LinkMovementMethod());
249 getListView().addFooterView(v);
250 getListView().setFooterDividersEnabled(false);
253 private boolean isFingerprintDisabled() {
254 final DevicePolicyManager dpm =
255 (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);
256 return dpm != null && (dpm.getKeyguardDisabledFeatures(null)
257 & DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) != 0;
260 protected void removeFingerprintPreference(int fingerprintId) {
261 String name = genKey(fingerprintId);
262 Preference prefToRemove = findPreference(name);
263 if (prefToRemove != null) {
264 if (!getPreferenceScreen().removePreference(prefToRemove)) {
265 Log.w(TAG, "Failed to remove preference with key " + name);
268 Log.w(TAG, "Can't find preference to remove: " + name);
275 * Don't forget to update the SecuritySearchIndexProvider if you are doing any change in the
276 * logic or adding/removing preferences here.
278 private PreferenceScreen createPreferenceHierarchy() {
279 PreferenceScreen root = getPreferenceScreen();
283 addPreferencesFromResource(R.xml.security_settings_fingerprint);
284 root = getPreferenceScreen();
285 addFingerprintItemPreferences(root);
289 private void addFingerprintItemPreferences(PreferenceGroup root) {
291 final List<Fingerprint> items = mFingerprintManager.getEnrolledFingerprints();
292 final int fingerprintCount = items.size();
293 for (int i = 0; i < fingerprintCount; i++) {
294 final Fingerprint item = items.get(i);
295 FingerprintPreference pref = new FingerprintPreference(root.getContext());
296 pref.setKey(genKey(item.getFingerId()));
297 pref.setTitle(item.getName());
298 pref.setFingerprint(item);
299 pref.setPersistent(false);
300 root.addPreference(pref);
301 pref.setOnPreferenceChangeListener(this);
303 Preference addPreference = new Preference(root.getContext());
304 addPreference.setKey(KEY_FINGERPRINT_ADD);
305 addPreference.setTitle(R.string.fingerprint_add_title);
306 addPreference.setIcon(R.drawable.ic_add_24dp);
307 root.addPreference(addPreference);
308 addPreference.setOnPreferenceChangeListener(this);
311 private static String genKey(int id) {
312 return KEY_FINGERPRINT_ITEM_PREFIX + "_" + id;
316 public void onResume() {
318 // Make sure we reload the preference hierarchy since fingerprints may be added,
319 // deleted or renamed.
323 private void updatePreferences() {
324 createPreferenceHierarchy();
325 retryFingerprint(true);
329 public void onPause() {
335 public void onSaveInstanceState(final Bundle outState) {
336 outState.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN,
338 outState.putBoolean(KEY_LAUNCHED_CONFIRM, mLaunchedConfirm);
342 public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference pref) {
343 final String key = pref.getKey();
344 if (KEY_FINGERPRINT_ADD.equals(key)) {
345 Intent intent = new Intent();
346 intent.setClassName("com.android.settings",
347 FingerprintEnrollEnrolling.class.getName());
348 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken);
350 startActivityForResult(intent, ADD_FINGERPRINT_REQUEST);
351 } else if (pref instanceof FingerprintPreference) {
352 FingerprintPreference fpref = (FingerprintPreference) pref;
353 final Fingerprint fp =fpref.getFingerprint();
354 showRenameDeleteDialog(pref, fp);
355 return super.onPreferenceTreeClick(preferenceScreen, pref);
360 private void showRenameDeleteDialog(Preference pref, final Fingerprint fp) {
361 final Activity activity = getActivity();
362 final AlertDialog dialog = new AlertDialog.Builder(activity)
363 .setView(R.layout.fingerprint_rename_dialog)
364 .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok,
365 new DialogInterface.OnClickListener() {
367 public void onClick(DialogInterface dialog, int which) {
368 final String newName = mDialogTextField.getText().toString();
369 final CharSequence name = fp.getName();
370 if (!newName.equals(name)) {
371 if (DEBUG) Log.v(TAG, "rename " + name + " to " + newName);
372 mFingerprintManager.rename(fp.getFingerId(), newName);
378 .setNegativeButton(R.string.security_settings_fingerprint_enroll_dialog_delete,
379 new DialogInterface.OnClickListener() {
381 public void onClick(DialogInterface dialog, int which) {
382 if (DEBUG) Log.v(TAG, "Removing fpId=" + fp.getFingerId());
383 mFingerprintManager.remove(fp, mRemoveCallback);
389 mDialogTextField = (EditText) dialog.findViewById(R.id.fingerprint_rename_field);
390 mDialogTextField.setText(fp.getName());
391 mDialogTextField.selectAll();
393 mDialogTextField.setOnFocusChangeListener(new View.OnFocusChangeListener() {
395 public void onFocusChange(View v, boolean hasFocus) {
397 dialog.getWindow().setSoftInputMode(
398 WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
405 public boolean onPreferenceChange(Preference preference, Object value) {
406 boolean result = true;
407 final String key = preference.getKey();
408 if (KEY_FINGERPRINT_ENABLE_KEYGUARD_TOGGLE.equals(key)) {
411 Log.v(TAG, "Unknown key:" + key);
417 protected int getHelpResource() {
418 return R.string.help_url_fingerprint;
422 public void onActivityResult(int requestCode, int resultCode, Intent data) {
423 super.onActivityResult(requestCode, resultCode, data);
424 if (requestCode == CHOOSE_LOCK_GENERIC_REQUEST
425 || requestCode == CONFIRM_REQUEST) {
426 if (resultCode == RESULT_FINISHED || resultCode == RESULT_OK) {
427 // The lock pin/pattern/password was set. Start enrolling!
429 mToken = data.getByteArrayExtra(
430 ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
435 if (mToken == null) {
436 // Didn't get an authentication, finishing
437 getActivity().finish();
441 private Drawable getHighlightDrawable() {
442 if (mHighlightDrawable == null) {
443 mHighlightDrawable = getActivity().getDrawable(R.drawable.preference_highlight);
445 return mHighlightDrawable;
448 private void highlightFingerprintItem(int fpId) {
449 String prefName = genKey(fpId);
450 FingerprintPreference fpref =
451 (FingerprintPreference) mManageCategory.findPreference(prefName);
452 final Drawable highlight = getHighlightDrawable();
453 final View view = fpref.getView();
454 final int centerX = view.getWidth() / 2;
455 final int centerY = view.getHeight() / 2;
456 highlight.setHotspot(centerX, centerY);
457 view.setBackground(highlight);
458 view.setPressed(true);
459 mHandler.postDelayed(new Runnable() {
462 view.setPressed(false);
463 view.setBackground(null);
465 }, RESET_HIGHLIGHT_DELAY_MS);
468 private void launchChooseOrConfirmLock() {
469 Intent intent = new Intent();
470 long challenge = mFingerprintManager.preEnroll();
471 ChooseLockSettingsHelper helper = new ChooseLockSettingsHelper(getActivity(), this);
472 if (!helper.launchConfirmationActivity(CONFIRM_REQUEST,
473 getString(R.string.security_settings_fingerprint_preference_title),
474 null, null, challenge)) {
475 intent.setClassName("com.android.settings", ChooseLockGeneric.class.getName());
476 intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.MINIMUM_QUALITY_KEY,
477 DevicePolicyManager.PASSWORD_QUALITY_SOMETHING);
478 intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_DISABLED_PREFS,
480 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, true);
481 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, challenge);
482 startActivityForResult(intent, CHOOSE_LOCK_GENERIC_REQUEST);
487 public static class FingerprintPreference extends Preference {
488 private Fingerprint mFingerprint;
491 public FingerprintPreference(Context context, AttributeSet attrs, int defStyleAttr,
493 super(context, attrs, defStyleAttr, defStyleRes);
495 public FingerprintPreference(Context context, AttributeSet attrs, int defStyleAttr) {
496 this(context, attrs, defStyleAttr, 0);
499 public FingerprintPreference(Context context, AttributeSet attrs) {
500 this(context, attrs, com.android.internal.R.attr.preferenceStyle);
503 public FingerprintPreference(Context context) {
507 public View getView() { return mView; }
509 public void setFingerprint(Fingerprint item) {
513 public Fingerprint getFingerprint() {
518 protected void onBindView(View view) {
519 super.onBindView(view);
524 private static class LearnMoreSpan extends URLSpan {
526 private static final Typeface TYPEFACE_MEDIUM =
527 Typeface.create("sans-serif-medium", Typeface.NORMAL);
529 private LearnMoreSpan(String url) {
534 public void onClick(View widget) {
535 Context ctx = widget.getContext();
536 Intent intent = HelpUtils.getHelpIntent(ctx, getURL());
538 ctx.startActivity(intent);
539 } catch (ActivityNotFoundException e) {
540 Log.w(FingerprintSettingsFragment.TAG,
541 "Actvity was not found for intent, " + intent.toString());
546 public void updateDrawState(TextPaint ds) {
547 super.updateDrawState(ds);
548 ds.setUnderlineText(false);
549 ds.setTypeface(TYPEFACE_MEDIUM);
552 public static CharSequence linkify(CharSequence rawText, String uri) {
553 SpannableString msg = new SpannableString(rawText);
554 Annotation[] spans = msg.getSpans(0, msg.length(), Annotation.class);
555 SpannableStringBuilder builder = new SpannableStringBuilder(msg);
556 for (Annotation annotation : spans) {
557 int start = msg.getSpanStart(annotation);
558 int end = msg.getSpanEnd(annotation);
559 LearnMoreSpan link = new LearnMoreSpan(uri);
560 builder.setSpan(link, start, end, msg.getSpanFlags(link));