diff --git a/res/layout/preference_widget_delete.xml b/res/layout/preference_widget_delete.xml new file mode 100644 index 00000000000..b62946c3513 --- /dev/null +++ b/res/layout/preference_widget_delete.xml @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index d4af6bfd50b..df7a08c6f65 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -920,6 +920,12 @@ Remove all fingerprints? + + Remove \'%1$s\' + + + Do you want to delete this fingerprint? + You won\'t be able to use your fingerprints to unlock your phone, authorize purchases, or sign in to apps with them diff --git a/src/com/android/settings/fingerprint/FingerprintSettings.java b/src/com/android/settings/fingerprint/FingerprintSettings.java index 89c3d78469f..2425c61dcce 100644 --- a/src/com/android/settings/fingerprint/FingerprintSettings.java +++ b/src/com/android/settings/fingerprint/FingerprintSettings.java @@ -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(); @@ -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 { diff --git a/tests/robotests/src/com/android/settings/fingerprint/DeleteFingerprintDialogTest.java b/tests/robotests/src/com/android/settings/fingerprint/DeleteFingerprintDialogTest.java new file mode 100644 index 00000000000..c3c1b42fefb --- /dev/null +++ b/tests/robotests/src/com/android/settings/fingerprint/DeleteFingerprintDialogTest.java @@ -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); + } +} diff --git a/tests/robotests/src/com/android/settings/fingerprint/FingerprintPreferenceTest.java b/tests/robotests/src/com/android/settings/fingerprint/FingerprintPreferenceTest.java new file mode 100644 index 00000000000..29c6f10d666 --- /dev/null +++ b/tests/robotests/src/com/android/settings/fingerprint/FingerprintPreferenceTest.java @@ -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); + } +}