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
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 325fc8d..9bbcf23 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -3234,6 +3234,9 @@
         <activity android:name=".homepage.contextualcards.ContextualCardFeedbackDialog"
                   android:theme="@android:style/Theme.DeviceDefault.Light.Dialog.Alert" />
 
+        <activity android:name=".homepage.contextualcards.FaceReEnrollDialog"
+                  android:theme="@android:style/Theme.DeviceDefault.Light.Dialog.Alert" />
+
         <activity
             android:name="Settings$WifiCallingDisclaimerActivity"
             android:label="@string/wifi_calling_settings_title"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 04ccd17..f1b592a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -954,6 +954,18 @@
     <string name="security_settings_face_enroll_finish_title">All set. Looking good.</string>
     <!-- Button text to exit face wizard after everything is done [CHAR LIMIT=15] -->
     <string name="security_settings_face_enroll_done">Done</string>
+    <!-- Settings suggestion title text for re-enrolling a face. [CHAR LIMIT=50] -->
+    <string name="security_settings_face_enroll_should_re_enroll_title">Improve face unlock performance</string>
+    <!-- Settings suggestion subtitle text for re-enrolling a face. [CHAR LIMIT=40] -->
+    <string name="security_settings_face_enroll_should_re_enroll_subtitle">Set up face unlock again</string>
+    <!-- Settings suggestion title text for mandatory re-enrolling of a face. [CHAR LIMIT=50] -->
+    <string name="security_settings_face_enroll_must_re_enroll_title">Set up face unlock again</string>
+    <!-- Settings suggestion subtitle text for mandatory re-enrolling of a face. [CHAR LIMIT=40] -->
+    <string name="security_settings_face_enroll_must_re_enroll_subtitle">Improve security and performance</string>
+    <!-- Settings suggestion alert body title for re-enrolling a face. [CHAR LIMIT=60] -->
+    <string name="security_settings_face_enroll_improve_face_alert_title">Set up face unlock</string>
+    <!-- Settings suggestion alert body text for re-enrolling a face. [CHAR LIMIT=300] -->
+    <string name="security_settings_face_enroll_improve_face_alert_body">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.</string>
     <!-- Title for a category shown for the face settings page. [CHAR LIMIT=20] -->
     <string name="security_settings_face_settings_use_face_category">Use face unlock for</string>
     <!-- Text shown on a toggle which allows or disallows the device to use face for unlocking the device. [CHAR LIMIT=20] -->
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 0000000..46ba26d
--- /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 112f655..2e34824 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.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 @@
 
     @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 CharSequence title = mContext.getText(
-                R.string.security_settings_face_settings_enroll);
+        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 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 @@
 
     @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 @@
                 .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 71b5c7a..9875ab4 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.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 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