Merge "Improve delete fingerprint UI."

This commit is contained in:
TreeHugger Robot
2017-06-27 20:27:46 +00:00
committed by Android (Google) Code Review
5 changed files with 327 additions and 73 deletions

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2017 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.
-->
<!-- Settings button -->
<ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/delete_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:paddingStart="?android:attr/listPreferredItemPaddingEnd"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:background="?android:attr/selectableItemBackground"
android:scaleType="center"
android:src="@drawable/ic_delete"
android:contentDescription="@string/delete" />

View File

@@ -920,6 +920,12 @@
<!-- Title shown in a dialog which asks the user to confirm when the last fingerprint gets deleted by him. [CHAR LIMIT=50]-->
<string name="fingerprint_last_delete_title">Remove all fingerprints?</string>
<!-- Title shown in a dialog which asks the user to confirm when a single fingerprint gets deleted. [CHAR LIMIT=50]-->
<string name="fingerprint_delete_title">Remove \'<xliff:g id="fingerprint_id" example="Fingerprint 2">%1$s</xliff:g>\'</string>
<!-- Message shown in a dialog which asks the user to confirm when a single fingerprint gets deleted. [CHAR LIMIT=NONE]-->
<string name="fingerprint_delete_message">Do you want to delete this fingerprint?</string>
<!-- Message shown in a dialog which asks the user to confirm when the last fingerprint gets deleted by him. [CHAR LIMIT=NONE]-->
<string name="fingerprint_last_delete_message">You won\'t be able to use your fingerprints to
unlock your phone, authorize purchases, or sign in to apps with them</string>

View File

@@ -36,6 +36,7 @@ import android.os.CancellationSignal;
import android.os.Handler;
import android.os.UserHandle;
import android.os.UserManager;
import android.support.annotation.VisibleForTesting;
import android.support.v7.preference.Preference;
import android.support.v7.preference.Preference.OnPreferenceChangeListener;
import android.support.v7.preference.Preference.OnPreferenceClickListener;
@@ -48,7 +49,6 @@ import android.text.SpannableStringBuilder;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.URLSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
@@ -66,6 +66,7 @@ import com.android.settings.password.ChooseLockSettingsHelper;
import com.android.settingslib.HelpUtils;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import com.android.settingslib.TwoTargetPreference;
import com.android.settingslib.widget.FooterPreference;
import java.util.HashMap;
@@ -127,7 +128,7 @@ public class FingerprintSettings extends SubSettings {
}
public static class FingerprintSettingsFragment extends SettingsPreferenceFragment
implements OnPreferenceChangeListener {
implements OnPreferenceChangeListener, FingerprintPreference.OnDeleteClickListener {
private static final int MAX_RETRY_ATTEMPTS = 20;
private static final int RESET_HIGHLIGHT_DELAY_MS = 500;
@@ -173,7 +174,7 @@ public class FingerprintSettings extends SubSettings {
@Override
public void onAuthenticationFailed() {
mHandler.obtainMessage(MSG_FINGER_AUTH_FAIL).sendToTarget();
};
}
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
@@ -195,6 +196,7 @@ public class FingerprintSettings extends SubSettings {
fingerprint.getFingerId(), 0).sendToTarget();
updateDialog();
}
public void onRemovalError(Fingerprint fp, int errMsgId, CharSequence errString) {
final Activity activity = getActivity();
if (activity != null) {
@@ -202,11 +204,12 @@ public class FingerprintSettings extends SubSettings {
}
updateDialog();
}
private void updateDialog() {
RenameDeleteDialog renameDeleteDialog = (RenameDeleteDialog)getFragmentManager().
findFragmentByTag(RenameDeleteDialog.class.getName());
if (renameDeleteDialog != null) {
renameDeleteDialog.enableDelete();
RenameDialog renameDialog = (RenameDialog) getFragmentManager().
findFragmentByTag(RenameDialog.class.getName());
if (renameDialog != null) {
renameDialog.enableDelete();
}
}
};
@@ -236,7 +239,7 @@ public class FingerprintSettings extends SubSettings {
}
break;
}
};
}
};
private void stopFingerprint() {
@@ -310,10 +313,10 @@ public class FingerprintSettings extends SubSettings {
mRemovalSidecar.setFingerprintManager(mFingerprintManager);
mRemovalSidecar.setListener(mRemovalListener);
RenameDeleteDialog renameDeleteDialog = (RenameDeleteDialog)getFragmentManager().
findFragmentByTag(RenameDeleteDialog.class.getName());
if (renameDeleteDialog != null) {
renameDeleteDialog.setDeleteInProgress(mRemovalSidecar.inProgress());
RenameDialog renameDialog = (RenameDialog) getFragmentManager().
findFragmentByTag(RenameDialog.class.getName());
if (renameDialog != null) {
renameDialog.setDeleteInProgress(mRemovalSidecar.inProgress());
}
mFingerprintsRenaming = new HashMap<Integer, String>();
@@ -380,7 +383,8 @@ public class FingerprintSettings extends SubSettings {
final int fingerprintCount = items.size();
for (int i = 0; i < fingerprintCount; i++) {
final Fingerprint item = items.get(i);
FingerprintPreference pref = new FingerprintPreference(root.getContext());
FingerprintPreference pref = new FingerprintPreference(root.getContext(),
this /* onDeleteClickListener */);
pref.setKey(genKey(item.getFingerId()));
pref.setTitle(item.getName());
pref.setFingerprint(item);
@@ -471,14 +475,40 @@ public class FingerprintSettings extends SubSettings {
} else if (pref instanceof FingerprintPreference) {
FingerprintPreference fpref = (FingerprintPreference) pref;
final Fingerprint fp = fpref.getFingerprint();
showRenameDeleteDialog(fp);
return super.onPreferenceTreeClick(pref);
showRenameDialog(fp);
}
return true;
return super.onPreferenceTreeClick(pref);
}
private void showRenameDeleteDialog(final Fingerprint fp) {
RenameDeleteDialog renameDeleteDialog = new RenameDeleteDialog();
@Override
public void onDeleteClick(FingerprintPreference p) {
final boolean hasMultipleFingerprint =
mFingerprintManager.getEnrolledFingerprints(mUserId).size() > 1;
final Fingerprint fp = p.getFingerprint();
if (hasMultipleFingerprint) {
if (mRemovalSidecar.inProgress()) {
Log.d(TAG, "Fingerprint delete in progress, skipping");
return;
}
DeleteFingerprintDialog.newInstance(fp, this /* target */)
.show(getFragmentManager(), DeleteFingerprintDialog.class.getName());
} else {
ConfirmLastDeleteDialog lastDeleteDialog = new ConfirmLastDeleteDialog();
final boolean isProfileChallengeUser =
UserManager.get(getContext()).isManagedProfile(mUserId);
final Bundle args = new Bundle();
args.putParcelable("fingerprint", fp);
args.putBoolean("isProfileChallengeUser", isProfileChallengeUser);
lastDeleteDialog.setArguments(args);
lastDeleteDialog.setTargetFragment(this, 0);
lastDeleteDialog.show(getFragmentManager(),
ConfirmLastDeleteDialog.class.getName());
}
}
private void showRenameDialog(final Fingerprint fp) {
RenameDialog renameDialog = new RenameDialog();
Bundle args = new Bundle();
if (mFingerprintsRenaming.containsKey(fp.getFingerId())) {
final Fingerprint f = new Fingerprint(mFingerprintsRenaming.get(fp.getFingerId()),
@@ -487,10 +517,10 @@ public class FingerprintSettings extends SubSettings {
} else {
args.putParcelable("fingerprint", fp);
}
renameDeleteDialog.setDeleteInProgress(mRemovalSidecar.inProgress());
renameDeleteDialog.setArguments(args);
renameDeleteDialog.setTargetFragment(this, 0);
renameDeleteDialog.show(getFragmentManager(), RenameDeleteDialog.class.getName());
renameDialog.setDeleteInProgress(mRemovalSidecar.inProgress());
renameDialog.setArguments(args);
renameDialog.setTargetFragment(this, 0);
renameDialog.show(getFragmentManager(), RenameDialog.class.getName());
}
@Override
@@ -598,7 +628,8 @@ public class FingerprintSettings extends SubSettings {
}
}
private void deleteFingerPrint(Fingerprint fingerPrint) {
@VisibleForTesting
void deleteFingerPrint(Fingerprint fingerPrint) {
mRemovalSidecar.startRemove(fingerPrint, mUserId);
String name = genKey(fingerPrint.getFingerId());
Preference prefToRemove = findPreference(name);
@@ -622,7 +653,60 @@ public class FingerprintSettings extends SubSettings {
}
};
public static class RenameDeleteDialog extends InstrumentedDialogFragment {
public static class DeleteFingerprintDialog extends InstrumentedDialogFragment
implements DialogInterface.OnClickListener {
private static final String KEY_FINGERPRINT = "fingerprint";
private Fingerprint mFp;
private AlertDialog mAlertDialog;
public static DeleteFingerprintDialog newInstance(Fingerprint fp,
FingerprintSettingsFragment target) {
final DeleteFingerprintDialog dialog = new DeleteFingerprintDialog();
final Bundle bundle = new Bundle();
bundle.putParcelable(KEY_FINGERPRINT, fp);
dialog.setArguments(bundle);
dialog.setTargetFragment(target, 0 /* requestCode */);
return dialog;
}
@Override
public int getMetricsCategory() {
return MetricsEvent.DIALOG_FINGERPINT_EDIT;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
mFp = getArguments().getParcelable(KEY_FINGERPRINT);
final String title = getString(R.string.fingerprint_delete_title, mFp.getName());
mAlertDialog = new AlertDialog.Builder(getActivity())
.setTitle(title)
.setMessage(R.string.fingerprint_delete_message)
.setPositiveButton(
R.string.security_settings_fingerprint_enroll_dialog_delete,
this /* onClickListener */)
.setNegativeButton(R.string.cancel, null /* onClickListener */)
.create();
return mAlertDialog;
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
final int fingerprintId = mFp.getFingerId();
Log.v(TAG, "Removing fpId=" + fingerprintId);
mMetricsFeatureProvider.action(getContext(),
MetricsEvent.ACTION_FINGERPRINT_DELETE,
fingerprintId);
FingerprintSettingsFragment parent
= (FingerprintSettingsFragment) getTargetFragment();
parent.deleteFingerPrint(mFp);
}
}
}
public static class RenameDialog extends InstrumentedDialogFragment {
private Fingerprint mFp;
private EditText mDialogTextField;
@@ -636,6 +720,7 @@ public class FingerprintSettings extends SubSettings {
public void setDeleteInProgress(boolean deleteInProgress) {
mDeleteInProgress = deleteInProgress;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
mFp = getArguments().getParcelable("fingerprint");
@@ -654,10 +739,8 @@ public class FingerprintSettings extends SubSettings {
final String newName =
mDialogTextField.getText().toString();
final CharSequence name = mFp.getName();
if (!newName.equals(name)) {
if (DEBUG) {
Log.v(TAG, "rename " + name + " to " + newName);
}
if (!TextUtils.equals(newName, name)) {
Log.d(TAG, "rename " + name + " to " + newName);
mMetricsFeatureProvider.action(getContext(),
MetricsEvent.ACTION_FINGERPRINT_RENAME,
mFp.getFingerId());
@@ -670,14 +753,6 @@ public class FingerprintSettings extends SubSettings {
dialog.dismiss();
}
})
.setNegativeButton(
R.string.security_settings_fingerprint_enroll_dialog_delete,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
onDeleteClick(dialog);
}
})
.create();
mAlertDialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
@@ -711,29 +786,6 @@ public class FingerprintSettings extends SubSettings {
}
}
private void onDeleteClick(DialogInterface dialog) {
if (DEBUG) Log.v(TAG, "Removing fpId=" + mFp.getFingerId());
mMetricsFeatureProvider.action(getContext(), MetricsEvent.ACTION_FINGERPRINT_DELETE,
mFp.getFingerId());
FingerprintSettingsFragment parent
= (FingerprintSettingsFragment) getTargetFragment();
final boolean isProfileChallengeUser =
UserManager.get(getContext()).isManagedProfile(parent.mUserId);
if (parent.mFingerprintManager.getEnrolledFingerprints(parent.mUserId).size() > 1) {
parent.deleteFingerPrint(mFp);
} else {
ConfirmLastDeleteDialog lastDeleteDialog = new ConfirmLastDeleteDialog();
Bundle args = new Bundle();
args.putParcelable("fingerprint", mFp);
args.putBoolean("isProfileChallengeUser", isProfileChallengeUser);
lastDeleteDialog.setArguments(args);
lastDeleteDialog.setTargetFragment(getTargetFragment(), 0);
lastDeleteDialog.show(getFragmentManager(),
ConfirmLastDeleteDialog.class.getName());
}
dialog.dismiss();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
@@ -794,27 +846,26 @@ public class FingerprintSettings extends SubSettings {
}
}
public static class FingerprintPreference extends Preference {
public static class FingerprintPreference extends TwoTargetPreference {
private final OnDeleteClickListener mOnDeleteClickListener;
private Fingerprint mFingerprint;
private View mView;
private View mDeleteView;
public FingerprintPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public FingerprintPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
public interface OnDeleteClickListener {
void onDeleteClick(FingerprintPreference p);
}
public FingerprintPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FingerprintPreference(Context context) {
public FingerprintPreference(Context context, OnDeleteClickListener onDeleteClickListener) {
super(context);
mOnDeleteClickListener = onDeleteClickListener;
}
public View getView() { return mView; }
public View getView() {
return mView;
}
public void setFingerprint(Fingerprint item) {
mFingerprint = item;
@@ -824,12 +875,26 @@ public class FingerprintSettings extends SubSettings {
return mFingerprint;
}
@Override
protected int getSecondTargetResId() {
return R.layout.preference_widget_delete;
}
@Override
public void onBindViewHolder(PreferenceViewHolder view) {
super.onBindViewHolder(view);
mView = view.itemView;
mDeleteView = view.itemView.findViewById(R.id.delete_button);
mDeleteView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mOnDeleteClickListener != null) {
mOnDeleteClickListener.onDeleteClick(FingerprintPreference.this);
}
}
});
}
};
}
private static class LearnMoreSpan extends URLSpan {

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2017 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.app.Dialog;
import android.hardware.fingerprint.Fingerprint;
import com.android.settings.TestConfig;
import com.android.settings.fingerprint.FingerprintSettings.FingerprintSettingsFragment;
import com.android.settings.fingerprint.FingerprintSettings.FingerprintSettingsFragment
.DeleteFingerprintDialog;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import com.android.settings.testutils.shadow.ShadowEventLogWriter;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.robolectric.util.FragmentTestUtil;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION,
shadows = {
ShadowEventLogWriter.class
})
public class DeleteFingerprintDialogTest {
@Mock
private FingerprintSettingsFragment mTarget;
@Mock
private Fingerprint mFingerprint;
private DeleteFingerprintDialog mFragment;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mFragment = DeleteFingerprintDialog.newInstance(mFingerprint, mTarget);
}
@Test
public void launchDialog_clickPositive_shouldDeleteFingerprint() {
FragmentTestUtil.startFragment(mFragment);
mFragment.onClick(mFragment.getDialog(), Dialog.BUTTON_POSITIVE);
verify(mTarget).deleteFingerPrint(mFingerprint);
}
@Test
public void launchDialog_clickNegative_shouldDoNothing() {
FragmentTestUtil.startFragment(mFragment);
mFragment.onClick(mFragment.getDialog(), Dialog.BUTTON_NEGATIVE);
verify(mTarget, never()).deleteFingerPrint(mFingerprint);
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright (C) 2017 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.content.Context;
import android.support.v7.preference.PreferenceViewHolder;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import com.android.settings.R;
import com.android.settings.TestConfig;
import com.android.settings.fingerprint.FingerprintSettings.FingerprintPreference;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.verify;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class FingerprintPreferenceTest {
@Mock
private FingerprintPreference.OnDeleteClickListener mOnDeleteClickListener;
private Context mContext;
private FingerprintPreference mPreference;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
mPreference = new FingerprintPreference(mContext, mOnDeleteClickListener);
}
@Test
public void shouldShowDeleteButton() {
assertThat(mPreference.getSecondTargetResId()).isEqualTo(R.layout.preference_widget_delete);
}
@Test
public void bindAndClickDeleteButton_shouldInvokeOnDeleteListener() {
final FrameLayout layout = new FrameLayout(mContext);
final View deleteButton = LayoutInflater.from(mContext)
.inflate(mPreference.getSecondTargetResId(), layout, true);
final PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(layout);
mPreference.onBindViewHolder(holder);
layout.findViewById(R.id.delete_button).performClick();
verify(mOnDeleteClickListener).onDeleteClick(mPreference);
}
}