Files
packages_apps_Settings/src/com/android/settings/fingerprint/FingerprintSettings.java
Jim Miller 5adada6083 Limit number of fingerprint templates that can be enrolled per device user
This change places an upper limit on the number of fingerprint templates that
can be enrolled per account.  This is done primarily for performance reasons, but
may also be imposed by hardware and Trusted Execution Environment (TEE) reasons.

Fixes bug 20731847

Change-Id: I3975290fa8eb2106467493aab6102015697e012c

 On branch max_fingerprint
2015-06-15 20:44:45 -07:00

607 lines
25 KiB
Java

/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.fingerprint;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.admin.DevicePolicyManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.hardware.fingerprint.Fingerprint;
import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintManager.AuthenticationCallback;
import android.hardware.fingerprint.FingerprintManager.AuthenticationResult;
import android.hardware.fingerprint.FingerprintManager.RemovalCallback;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Handler;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.PreferenceGroup;
import android.preference.PreferenceScreen;
import android.provider.Browser;
import android.text.Annotation;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextPaint;
import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import com.android.internal.logging.MetricsLogger;
import com.android.settings.ChooseLockGeneric;
import com.android.settings.ChooseLockSettingsHelper;
import com.android.settings.HelpUtils;
import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.SubSettings;
import com.android.settings.search.Indexable;
import java.util.List;
/**
* Settings screen for fingerprints
*/
public class FingerprintSettings extends SubSettings {
/**
* Used by the FP settings wizard to indicate the wizard is
* finished, and each activity in the wizard should finish.
* <p>
* Previously, each activity in the wizard would finish itself after
* starting the next activity. However, this leads to broken 'Back'
* behavior. So, now an activity does not finish itself until it gets this
* result.
*/
static final int RESULT_FINISHED = RESULT_FIRST_USER;
@Override
public Intent getIntent() {
Intent modIntent = new Intent(super.getIntent());
modIntent.putExtra(EXTRA_SHOW_FRAGMENT, FingerprintSettingsFragment.class.getName());
return modIntent;
}
@Override
protected boolean isValidFragment(String fragmentName) {
if (FingerprintSettingsFragment.class.getName().equals(fragmentName)) return true;
return false;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
CharSequence msg = getText(R.string.security_settings_fingerprint_preference_title);
setTitle(msg);
}
public static class FingerprintSettingsFragment extends SettingsPreferenceFragment
implements OnPreferenceChangeListener, Indexable {
private static final int MAX_RETRY_ATTEMPTS = 20;
private static final int RESET_HIGHLIGHT_DELAY_MS = 500;
private static final String TAG = "FingerprintSettings";
private static final String KEY_FINGERPRINT_ITEM_PREFIX = "key_fingerprint_item";
private static final String KEY_FINGERPRINT_ADD = "key_fingerprint_add";
private static final String KEY_FINGERPRINT_ENABLE_KEYGUARD_TOGGLE =
"fingerprint_enable_keyguard_toggle";
private static final String KEY_LAUNCHED_CONFIRM = "launched_confirm";
private static final int MSG_REFRESH_FINGERPRINT_TEMPLATES = 1000;
private static final int MSG_FINGER_AUTH_SUCCESS = 1001;
private static final int MSG_FINGER_AUTH_FAIL = 1002;
private static final int MSG_FINGER_AUTH_ERROR = 1003;
private static final int MSG_FINGER_AUTH_HELP = 1004;
private static final int CONFIRM_REQUEST = 101;
private static final int CHOOSE_LOCK_GENERIC_REQUEST = 102;
private static final int ADD_FINGERPRINT_REQUEST = 10;
protected static final boolean DEBUG = true;
private FingerprintManager mFingerprintManager;
private EditText mDialogTextField;
private CancellationSignal mFingerprintCancel;
private int mMaxFingerprintAttempts;
private byte[] mToken;
private boolean mLaunchedConfirm;
private Drawable mHighlightDrawable;
private AuthenticationCallback mAuthCallback = new AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(AuthenticationResult result) {
int fingerId = result.getFingerprint().getFingerId();
mHandler.obtainMessage(MSG_FINGER_AUTH_SUCCESS, fingerId, 0).sendToTarget();
}
public void onAuthenticationFailed() {
mHandler.obtainMessage(MSG_FINGER_AUTH_FAIL).sendToTarget();
};
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
mHandler.obtainMessage(MSG_FINGER_AUTH_ERROR, errMsgId, 0, errString)
.sendToTarget();
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
mHandler.obtainMessage(MSG_FINGER_AUTH_HELP, helpMsgId, 0, helpString)
.sendToTarget();
}
};
private RemovalCallback mRemoveCallback = new RemovalCallback() {
@Override
public void onRemovalSucceeded(Fingerprint fingerprint) {
mHandler.obtainMessage(MSG_REFRESH_FINGERPRINT_TEMPLATES,
fingerprint.getFingerId(), 0).sendToTarget();
}
@Override
public void onRemovalError(Fingerprint fp, int errMsgId, CharSequence errString) {
final Activity activity = getActivity();
if (activity != null) {
Toast.makeText(activity, errString, Toast.LENGTH_SHORT);
}
}
};
private final Handler mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case MSG_REFRESH_FINGERPRINT_TEMPLATES:
removeFingerprintPreference(msg.arg1);
updateAddPreference();
break;
case MSG_FINGER_AUTH_SUCCESS:
mFingerprintCancel = null;
highlightFingerprintItem(msg.arg1);
retryFingerprint(true);
break;
case MSG_FINGER_AUTH_FAIL:
mFingerprintCancel = null;
retryFingerprint(true);
break;
case MSG_FINGER_AUTH_ERROR: {
mFingerprintCancel = null;
// get activity will be null on a screen rotation
final Activity activity = getActivity();
if (activity != null) {
CharSequence errString = (CharSequence) msg.obj;
Toast.makeText(activity, errString , Toast.LENGTH_SHORT);
}
final int errMsgId = msg.arg1;
if (errMsgId != FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
retryFingerprint(false);
}
}
break;
case MSG_FINGER_AUTH_HELP: {
final Activity activity = getActivity();
if (activity != null) {
CharSequence helpString = (CharSequence) msg.obj;
Toast.makeText(activity, helpString , Toast.LENGTH_SHORT);
}
}
break;
}
};
};
private void stopFingerprint() {
if (mFingerprintCancel != null) {
mFingerprintCancel.cancel();
mFingerprintCancel = null;
}
}
private void retryFingerprint(boolean resetAttempts) {
if (resetAttempts) {
mMaxFingerprintAttempts = 0;
}
if (mMaxFingerprintAttempts < MAX_RETRY_ATTEMPTS && mFingerprintCancel == null) {
mFingerprintCancel = new CancellationSignal();
mFingerprintManager.authenticate(null, mFingerprintCancel, 0 /* flags */,
mAuthCallback, null);
}
mMaxFingerprintAttempts++;
}
@Override
protected int getMetricsCategory() {
return MetricsLogger.FINGERPRINT;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mToken = savedInstanceState.getByteArray(
ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
mLaunchedConfirm = savedInstanceState.getBoolean(
KEY_LAUNCHED_CONFIRM, false);
}
Activity activity = getActivity();
mFingerprintManager = (FingerprintManager) activity.getSystemService(
Context.FINGERPRINT_SERVICE);
// Need to authenticate a session token if none
if (mToken == null && mLaunchedConfirm == false) {
mLaunchedConfirm = true;
launchChooseOrConfirmLock();
}
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
TextView v = (TextView) LayoutInflater.from(view.getContext()).inflate(
R.layout.fingerprint_settings_footer, null);
v.setText(LearnMoreSpan.linkify(getText(isFingerprintDisabled()
? R.string.security_settings_fingerprint_enroll_disclaimer_lockscreen_disabled
: R.string.security_settings_fingerprint_enroll_disclaimer),
getString(getHelpResource())));
v.setMovementMethod(new LinkMovementMethod());
getListView().addFooterView(v);
getListView().setFooterDividersEnabled(false);
}
private boolean isFingerprintDisabled() {
final DevicePolicyManager dpm =
(DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);
return dpm != null && (dpm.getKeyguardDisabledFeatures(null)
& DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) != 0;
}
protected void removeFingerprintPreference(int fingerprintId) {
String name = genKey(fingerprintId);
Preference prefToRemove = findPreference(name);
if (prefToRemove != null) {
if (!getPreferenceScreen().removePreference(prefToRemove)) {
Log.w(TAG, "Failed to remove preference with key " + name);
}
} else {
Log.w(TAG, "Can't find preference to remove: " + name);
}
}
/**
* Important!
*
* Don't forget to update the SecuritySearchIndexProvider if you are doing any change in the
* logic or adding/removing preferences here.
*/
private PreferenceScreen createPreferenceHierarchy() {
PreferenceScreen root = getPreferenceScreen();
if (root != null) {
root.removeAll();
}
addPreferencesFromResource(R.xml.security_settings_fingerprint);
root = getPreferenceScreen();
addFingerprintItemPreferences(root);
return root;
}
private void addFingerprintItemPreferences(PreferenceGroup root) {
root.removeAll();
final List<Fingerprint> items = mFingerprintManager.getEnrolledFingerprints();
final int fingerprintCount = items.size();
for (int i = 0; i < fingerprintCount; i++) {
final Fingerprint item = items.get(i);
FingerprintPreference pref = new FingerprintPreference(root.getContext());
pref.setKey(genKey(item.getFingerId()));
pref.setTitle(item.getName());
pref.setFingerprint(item);
pref.setPersistent(false);
root.addPreference(pref);
pref.setOnPreferenceChangeListener(this);
}
Preference addPreference = new Preference(root.getContext());
addPreference.setKey(KEY_FINGERPRINT_ADD);
addPreference.setTitle(R.string.fingerprint_add_title);
addPreference.setIcon(R.drawable.ic_add_24dp);
root.addPreference(addPreference);
addPreference.setOnPreferenceChangeListener(this);
updateAddPreference();
}
private void updateAddPreference() {
/* Disable preference if too many fingerprints added */
final int max = getContext().getResources().getInteger(
com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser);
boolean tooMany = mFingerprintManager.getEnrolledFingerprints().size() >= max;
CharSequence maxSummary = tooMany ?
getContext().getString(R.string.fingerprint_add_max, max) : "";
Preference addPreference = findPreference(KEY_FINGERPRINT_ADD);
addPreference.setSummary(maxSummary);
addPreference.setEnabled(!tooMany);
}
private static String genKey(int id) {
return KEY_FINGERPRINT_ITEM_PREFIX + "_" + id;
}
@Override
public void onResume() {
super.onResume();
// Make sure we reload the preference hierarchy since fingerprints may be added,
// deleted or renamed.
updatePreferences();
}
private void updatePreferences() {
createPreferenceHierarchy();
retryFingerprint(true);
}
@Override
public void onPause() {
super.onPause();
stopFingerprint();
}
@Override
public void onSaveInstanceState(final Bundle outState) {
outState.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN,
mToken);
outState.putBoolean(KEY_LAUNCHED_CONFIRM, mLaunchedConfirm);
}
@Override
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference pref) {
final String key = pref.getKey();
if (KEY_FINGERPRINT_ADD.equals(key)) {
Intent intent = new Intent();
intent.setClassName("com.android.settings",
FingerprintEnrollEnrolling.class.getName());
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken);
startActivityForResult(intent, ADD_FINGERPRINT_REQUEST);
} else if (pref instanceof FingerprintPreference) {
FingerprintPreference fpref = (FingerprintPreference) pref;
final Fingerprint fp =fpref.getFingerprint();
showRenameDeleteDialog(pref, fp);
return super.onPreferenceTreeClick(preferenceScreen, pref);
}
return true;
}
private void showRenameDeleteDialog(Preference pref, final Fingerprint fp) {
final Activity activity = getActivity();
final AlertDialog dialog = new AlertDialog.Builder(activity)
.setView(R.layout.fingerprint_rename_dialog)
.setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
final String newName = mDialogTextField.getText().toString();
final CharSequence name = fp.getName();
if (!newName.equals(name)) {
if (DEBUG) Log.v(TAG, "rename " + name + " to " + newName);
mFingerprintManager.rename(fp.getFingerId(), newName);
updatePreferences();
}
dialog.dismiss();
}
})
.setNegativeButton(R.string.security_settings_fingerprint_enroll_dialog_delete,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (DEBUG) Log.v(TAG, "Removing fpId=" + fp.getFingerId());
mFingerprintManager.remove(fp, mRemoveCallback);
dialog.dismiss();
}
})
.create();
dialog.show();
mDialogTextField = (EditText) dialog.findViewById(R.id.fingerprint_rename_field);
mDialogTextField.setText(fp.getName());
mDialogTextField.selectAll();
// show the IME
mDialogTextField.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
dialog.getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
}
}
});
}
@Override
public boolean onPreferenceChange(Preference preference, Object value) {
boolean result = true;
final String key = preference.getKey();
if (KEY_FINGERPRINT_ENABLE_KEYGUARD_TOGGLE.equals(key)) {
// TODO
} else {
Log.v(TAG, "Unknown key:" + key);
}
return result;
}
@Override
protected int getHelpResource() {
return R.string.help_url_fingerprint;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CHOOSE_LOCK_GENERIC_REQUEST
|| requestCode == CONFIRM_REQUEST) {
if (resultCode == RESULT_FINISHED || resultCode == RESULT_OK) {
// The lock pin/pattern/password was set. Start enrolling!
if (data != null) {
mToken = data.getByteArrayExtra(
ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
}
}
}
if (mToken == null) {
// Didn't get an authentication, finishing
getActivity().finish();
}
}
private Drawable getHighlightDrawable() {
if (mHighlightDrawable == null) {
final Activity activity = getActivity();
if (activity != null) {
mHighlightDrawable = activity.getDrawable(R.drawable.preference_highlight);
}
}
return mHighlightDrawable;
}
private void highlightFingerprintItem(int fpId) {
String prefName = genKey(fpId);
FingerprintPreference fpref = (FingerprintPreference) findPreference(prefName);
final Drawable highlight = getHighlightDrawable();
if (highlight != null) {
final View view = fpref.getView();
final int centerX = view.getWidth() / 2;
final int centerY = view.getHeight() / 2;
highlight.setHotspot(centerX, centerY);
view.setBackground(highlight);
view.setPressed(true);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
view.setPressed(false);
view.setBackground(null);
}
}, RESET_HIGHLIGHT_DELAY_MS);
}
}
private void launchChooseOrConfirmLock() {
Intent intent = new Intent();
long challenge = mFingerprintManager.preEnroll();
ChooseLockSettingsHelper helper = new ChooseLockSettingsHelper(getActivity(), this);
if (!helper.launchConfirmationActivity(CONFIRM_REQUEST,
getString(R.string.security_settings_fingerprint_preference_title),
null, null, challenge)) {
intent.setClassName("com.android.settings", ChooseLockGeneric.class.getName());
intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.MINIMUM_QUALITY_KEY,
DevicePolicyManager.PASSWORD_QUALITY_SOMETHING);
intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_DISABLED_PREFS,
true);
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, true);
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, challenge);
startActivityForResult(intent, CHOOSE_LOCK_GENERIC_REQUEST);
}
}
}
public static class FingerprintPreference extends Preference {
private Fingerprint mFingerprint;
private View mView;
public FingerprintPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public FingerprintPreference(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public FingerprintPreference(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.preferenceStyle);
}
public FingerprintPreference(Context context) {
this(context, null);
}
public View getView() { return mView; }
public void setFingerprint(Fingerprint item) {
mFingerprint = item;
}
public Fingerprint getFingerprint() {
return mFingerprint;
}
@Override
protected void onBindView(View view) {
super.onBindView(view);
mView = view;
}
};
private static class LearnMoreSpan extends URLSpan {
private static final Typeface TYPEFACE_MEDIUM =
Typeface.create("sans-serif-medium", Typeface.NORMAL);
private LearnMoreSpan(String url) {
super(url);
}
@Override
public void onClick(View widget) {
Context ctx = widget.getContext();
Intent intent = HelpUtils.getHelpIntent(ctx, getURL(), ctx.getClass().getName());
try {
ctx.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.w(FingerprintSettingsFragment.TAG,
"Actvity was not found for intent, " + intent.toString());
}
}
@Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false);
ds.setTypeface(TYPEFACE_MEDIUM);
}
public static CharSequence linkify(CharSequence rawText, String uri) {
SpannableString msg = new SpannableString(rawText);
Annotation[] spans = msg.getSpans(0, msg.length(), Annotation.class);
SpannableStringBuilder builder = new SpannableStringBuilder(msg);
for (Annotation annotation : spans) {
int start = msg.getSpanStart(annotation);
int end = msg.getSpanEnd(annotation);
LearnMoreSpan link = new LearnMoreSpan(uri);
builder.setSpan(link, start, end, msg.getSpanFlags(link));
}
return builder;
}
}
}