From bba7632f2847683b1b2044862c89a162de8398c7 Mon Sep 17 00:00:00 2001 From: joshmccloskey Date: Thu, 19 Sep 2019 11:23:29 -0700 Subject: [PATCH] Added different flow for re-enrollment In order to enable this new flow, a user must currently have an enrolled face and the security setting face_unlock_re_enroll must be non-zero. Ex. 1. Enroll Face. 2. adb shell settings put (secure face_unlock_re_enroll|secure_face_unlock_must_re_enroll) 1 3. If settings is opened, close it. 4. Open settings 5. Verify the new flow appears. Bug: 141380252 Bug: 141254937 Test: Verified that the user's face is deleted after clicking delete. Test: Verified that the user can re-enroll after removing their face. Change-Id: I2b36a0bda5cb10fb33dfb2a5627d8fa40f14fb7e --- AndroidManifest.xml | 3 + res/values/strings.xml | 12 ++ .../contextualcards/FaceReEnrollDialog.java | 122 +++++++++++++++++++++ .../contextualcards/slices/FaceSetupSlice.java | 97 ++++++++++++---- .../contextualcards/slices/FaceSetupSliceTest.java | 37 ++++++- 5 files changed, 250 insertions(+), 21 deletions(-) create mode 100644 src/com/android/settings/homepage/contextualcards/FaceReEnrollDialog.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 325fc8deca..9bbcf2341a 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3234,6 +3234,9 @@ + + All set. Looking good. Done + + Improve face unlock performance + + Set up face unlock again + + Set up face unlock again + + Improve security and performance + + Set up face unlock + + Delete your current face data to set up face unlock again.\n\nThe face data used by face unlock will be permanently and securely deleted. After removal, you will need your PIN, pattern, or password to unlock your phone, sign in to apps, and confirm payments. Use face unlock for diff --git a/src/com/android/settings/homepage/contextualcards/FaceReEnrollDialog.java b/src/com/android/settings/homepage/contextualcards/FaceReEnrollDialog.java new file mode 100644 index 0000000000..46ba26d82d --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/FaceReEnrollDialog.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2019 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.homepage.contextualcards; + + +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.hardware.face.Face; +import android.hardware.face.FaceManager; +import android.os.Bundle; +import android.provider.Settings; +import android.util.Log; + +import com.android.internal.app.AlertActivity; +import com.android.internal.app.AlertController; +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.homepage.contextualcards.slices.FaceSetupSlice; + +/** + * This class is used to show a popup dialog for {@link FaceSetupSlice}. + */ +public class FaceReEnrollDialog extends AlertActivity implements + DialogInterface.OnClickListener { + + private static final String TAG = "FaceReEnrollDialog"; + + private static final String BIOMETRIC_ENROLL_ACTION = "android.settings.BIOMETRIC_ENROLL"; + + private FaceManager mFaceManager; + /** + * The type of re-enrollment that has been requested, + * see {@link Settings.Secure#FACE_UNLOCK_RE_ENROLL} for more details. + */ + private int mReEnrollType; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final AlertController.AlertParams alertParams = mAlertParams; + alertParams.mTitle = getText( + R.string.security_settings_face_enroll_improve_face_alert_title); + alertParams.mMessage = getText( + R.string.security_settings_face_enroll_improve_face_alert_body); + alertParams.mPositiveButtonText = getText(R.string.storage_menu_set_up); + alertParams.mNegativeButtonText = getText(R.string.cancel); + alertParams.mPositiveButtonListener = this; + + mFaceManager = Utils.getFaceManagerOrNull(getApplicationContext()); + + final Context context = getApplicationContext(); + mReEnrollType = FaceSetupSlice.getReEnrollSetting(context, getUserId()); + + Log.d(TAG, "ReEnroll Type : " + mReEnrollType); + if (mReEnrollType == FaceSetupSlice.FACE_RE_ENROLL_SUGGESTED) { + // setupAlert will actually display the popup dialog. + setupAlert(); + } else if (mReEnrollType == FaceSetupSlice.FACE_RE_ENROLL_REQUIRED) { + // in this case we are skipping the popup dialog and directly going to the + // re enrollment flow. A grey overlay will appear to indicate that we are + // transitioning. + removeFaceAndReEnroll(); + } else { + Log.d(TAG, "Error unsupported flow for : " + mReEnrollType); + dismiss(); + } + } + + @Override + public void onClick(DialogInterface dialog, int which) { + removeFaceAndReEnroll(); + } + + public void removeFaceAndReEnroll() { + final int userId = getUserId(); + if (mFaceManager == null || !mFaceManager.hasEnrolledTemplates(userId)) { + finish(); + } + mFaceManager.remove(new Face("", 0, 0), userId, new FaceManager.RemovalCallback() { + @Override + public void onRemovalError(Face face, int errMsgId, CharSequence errString) { + super.onRemovalError(face, errMsgId, errString); + finish(); + } + + @Override + public void onRemovalSucceeded(Face face, int remaining) { + super.onRemovalSucceeded(face, remaining); + if (remaining != 0) { + return; + } + // Send user to the enroll flow. + final Intent reEnroll = new Intent(BIOMETRIC_ENROLL_ACTION); + final Context context = getApplicationContext(); + + try { + startActivity(reEnroll); + } catch (Exception e) { + Log.e(TAG, "Failed to startActivity"); + } + + finish(); + } + }); + } +} diff --git a/src/com/android/settings/homepage/contextualcards/slices/FaceSetupSlice.java b/src/com/android/settings/homepage/contextualcards/slices/FaceSetupSlice.java index 112f655701..2e34824017 100644 --- a/src/com/android/settings/homepage/contextualcards/slices/FaceSetupSlice.java +++ b/src/com/android/settings/homepage/contextualcards/slices/FaceSetupSlice.java @@ -17,17 +17,14 @@ package com.android.settings.homepage.contextualcards.slices; -import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_SUCCESS; - import android.app.PendingIntent; import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; -import android.hardware.biometrics.BiometricManager; import android.hardware.face.FaceManager; import android.net.Uri; import android.os.UserHandle; +import android.provider.Settings; import androidx.core.graphics.drawable.IconCompat; import androidx.slice.Slice; @@ -39,14 +36,40 @@ import com.android.settings.R; import com.android.settings.SubSettings; import com.android.settings.Utils; import com.android.settings.biometrics.face.FaceStatusPreferenceController; +import com.android.settings.homepage.contextualcards.FaceReEnrollDialog; import com.android.settings.security.SecuritySettings; import com.android.settings.slices.CustomSliceRegistry; import com.android.settings.slices.CustomSliceable; import com.android.settings.slices.SliceBuilderUtils; +/** + * This class is used for showing re-enroll suggestions in the Settings page. By either having an + * un-enrolled user or setting {@link Settings.Secure#FACE_UNLOCK_RE_ENROLL} to one of the + * states listed in {@link Settings.Secure} the slice will change its text and potentially show + * a {@link FaceReEnrollDialog}. + */ public class FaceSetupSlice implements CustomSliceable { private final Context mContext; + /** + * If a user currently is not enrolled then this class will show a recommendation to + * enroll their face. + */ + private FaceManager mFaceManager; + + /** + * Various states the {@link FaceSetupSlice} can be in, + * See {@link Settings.Secure#FACE_UNLOCK_RE_ENROLL} for more details. + */ + + // No re-enrollment. + public static final int FACE_NO_RE_ENROLL_REQUIRED = 0; + // Re enrollment is suggested. + public static final int FACE_RE_ENROLL_SUGGESTED = 1; + // Re enrollment is required after a set time period. + public static final int FACE_RE_ENROLL_AFTER_TIMEOUT = 2; + // Re enrollment is required immediately. + public static final int FACE_RE_ENROLL_REQUIRED = 3; public FaceSetupSlice(Context context) { mContext = context; @@ -54,21 +77,45 @@ public class FaceSetupSlice implements CustomSliceable { @Override public Slice getSlice() { - final FaceManager faceManager = Utils.getFaceManagerOrNull(mContext); - if (faceManager == null || faceManager.hasEnrolledTemplates(UserHandle.myUserId())) { - return null; + mFaceManager = Utils.getFaceManagerOrNull(mContext); + if (mFaceManager == null) { + return new ListBuilder(mContext, CustomSliceRegistry.FACE_ENROLL_SLICE_URI, + ListBuilder.INFINITY).setIsError(true).build(); + } + + final int userId = UserHandle.myUserId(); + final boolean hasEnrolledTemplates = mFaceManager.hasEnrolledTemplates(userId); + final int shouldReEnroll = FaceSetupSlice.getReEnrollSetting(mContext, userId); + + CharSequence title = ""; + CharSequence subtitle = ""; + + // Set the title and subtitle according to the different states, the icon and layout will + // stay the same. + if (!hasEnrolledTemplates) { + title = mContext.getText(R.string.security_settings_face_settings_enroll); + subtitle = mContext.getText( + R.string.security_settings_face_settings_context_subtitle); + } else if (shouldReEnroll == FACE_RE_ENROLL_SUGGESTED) { + title = mContext.getText( + R.string.security_settings_face_enroll_should_re_enroll_title); + subtitle = mContext.getText( + R.string.security_settings_face_enroll_should_re_enroll_subtitle); + } else if (shouldReEnroll == FACE_RE_ENROLL_REQUIRED) { + title = mContext.getText( + R.string.security_settings_face_enroll_must_re_enroll_title); + subtitle = mContext.getText( + R.string.security_settings_face_enroll_must_re_enroll_subtitle); + } else { + return new ListBuilder(mContext, CustomSliceRegistry.FACE_ENROLL_SLICE_URI, + ListBuilder.INFINITY).setIsError(true).build(); } - final CharSequence title = mContext.getText( - R.string.security_settings_face_settings_enroll); final ListBuilder listBuilder = new ListBuilder(mContext, CustomSliceRegistry.FACE_ENROLL_SLICE_URI, ListBuilder.INFINITY) .setAccentColor(Utils.getColorAccentDefaultColor(mContext)); final IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.ic_face_24dp); - return listBuilder - .addRow(buildRowBuilder(title, - mContext.getText(R.string.security_settings_face_settings_context_subtitle), - icon, mContext, getIntent())) + return listBuilder.addRow(buildRowBuilder(title, subtitle, icon, mContext, getIntent())) .build(); } @@ -79,12 +126,18 @@ public class FaceSetupSlice implements CustomSliceable { @Override public Intent getIntent() { - return SliceBuilderUtils.buildSearchResultPageIntent(mContext, - SecuritySettings.class.getName(), - FaceStatusPreferenceController.KEY_FACE_SETTINGS, - mContext.getText(R.string.security_settings_face_settings_enroll).toString(), - SettingsEnums.SLICE) - .setClassName(mContext.getPackageName(), SubSettings.class.getName()); + final boolean hasEnrolledTemplates = mFaceManager.hasEnrolledTemplates( + UserHandle.myUserId()); + if (!hasEnrolledTemplates) { + return SliceBuilderUtils.buildSearchResultPageIntent(mContext, + SecuritySettings.class.getName(), + FaceStatusPreferenceController.KEY_FACE_SETTINGS, + mContext.getText(R.string.security_settings_face_settings_enroll).toString(), + SettingsEnums.SLICE) + .setClassName(mContext.getPackageName(), SubSettings.class.getName()); + } else { + return new Intent(mContext, FaceReEnrollDialog.class); + } } private static RowBuilder buildRowBuilder(CharSequence title, CharSequence subTitle, @@ -98,4 +151,10 @@ public class FaceSetupSlice implements CustomSliceable { .setSubtitle(subTitle) .setPrimaryAction(primarySliceAction); } + + public static int getReEnrollSetting(Context context, int userId) { + return Settings.Secure.getIntForUser(context.getContentResolver(), + Settings.Secure.FACE_UNLOCK_RE_ENROLL, FACE_NO_RE_ENROLL_REQUIRED, userId); + } + } \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/FaceSetupSliceTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/FaceSetupSliceTest.java index 71b5c7a9b7..9875ab4924 100644 --- a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/FaceSetupSliceTest.java +++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/FaceSetupSliceTest.java @@ -26,6 +26,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.hardware.face.FaceManager; import android.os.UserHandle; +import android.provider.Settings; import androidx.slice.Slice; import androidx.slice.SliceProvider; @@ -59,26 +60,58 @@ public class FaceSetupSliceTest { public void getSlice_noFaceManager_shouldReturnNull() { when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(false); final FaceSetupSlice setupSlice = new FaceSetupSlice(mContext); + assertThat(setupSlice.getSlice()).isNull(); } @Test - public void getSlice_faceEnrolled_shouldReturnNull() { + public void getSlice_faceEnrolled_noReEnroll_shouldReturnNull() { final FaceManager faceManager = mock(FaceManager.class); when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); when(faceManager.hasEnrolledTemplates(UserHandle.myUserId())).thenReturn(true); when(mContext.getSystemService(Context.FACE_SERVICE)).thenReturn(faceManager); + Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.FACE_UNLOCK_RE_ENROLL, + 0); final FaceSetupSlice setupSlice = new FaceSetupSlice(mContext); + assertThat(setupSlice.getSlice()).isNull(); } @Test - public void getSlice_faceNotEnrolled_shouldReturnNonNull() { + public void getSlice_faceNotEnrolled_shouldReturnSlice() { final FaceManager faceManager = mock(FaceManager.class); when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); when(faceManager.hasEnrolledTemplates(UserHandle.myUserId())).thenReturn(false); when(mContext.getSystemService(Context.FACE_SERVICE)).thenReturn(faceManager); final FaceSetupSlice setupSlice = new FaceSetupSlice(mContext); + + assertThat(setupSlice.getSlice()).isNotNull(); + } + + @Test + public void getSlice_faceEnrolled_shouldReEnroll_shouldReturnSlice() { + final FaceManager faceManager = mock(FaceManager.class); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); + when(faceManager.hasEnrolledTemplates(UserHandle.myUserId())).thenReturn(true); + when(mContext.getSystemService(Context.FACE_SERVICE)).thenReturn(faceManager); + Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.FACE_UNLOCK_RE_ENROLL, + 1); + final FaceSetupSlice setupSlice = new FaceSetupSlice(mContext); + + assertThat(setupSlice.getSlice()).isNotNull(); + } + + @Test + public void getSlice_faceEnrolled_musteEnroll_shouldReturnSlice() { + final FaceManager faceManager = mock(FaceManager.class); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true); + when(faceManager.hasEnrolledTemplates(UserHandle.myUserId())).thenReturn(true); + when(mContext.getSystemService(Context.FACE_SERVICE)).thenReturn(faceManager); + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.FACE_UNLOCK_MUST_RE_ENROLL, + 1); + final FaceSetupSlice setupSlice = new FaceSetupSlice(mContext); + assertThat(setupSlice.getSlice()).isNotNull(); } } \ No newline at end of file -- 2.11.0