4/n: Add basic enrollment for Face

Bug: 110589286

Test: fingerprint enrolling still works
Test: enrollment flow with and without a pin set up still works properly
Test: enrollment continues when configuration changes, stops otherwise

Change-Id: I39f76c7f1a16e9533cef573f87cf4b81cb20cb18
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 3c635eb..35a619c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1570,6 +1570,8 @@
             android:theme="@style/GlifTheme.Light"/>
 
         <activity android:name=".biometrics.face.FaceEnrollIntroduction" android:exported="false" />
+        <activity android:name=".biometrics.face.FaceEnrollEnrolling" android:exported="false" />
+        <activity android:name=".biometrics.face.FaceEnrollFinish" android:exported="false" />
 
         <activity android:name=".biometrics.fingerprint.FingerprintSettings" android:exported="false"/>
         <activity android:name=".biometrics.fingerprint.FingerprintEnrollFindSensor" android:exported="false"/>
diff --git a/res/layout/face_enroll_enrolling.xml b/res/layout/face_enroll_enrolling.xml
new file mode 100644
index 0000000..6ced80f
--- /dev/null
+++ b/res/layout/face_enroll_enrolling.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 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.
+  -->
+
+<com.android.setupwizardlib.GlifLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/setup_wizard_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    style="?attr/face_layout_theme"
+    app:suwFooter="@layout/face_enroll_enrolling_footer">
+
+    <LinearLayout
+        style="@style/SuwContentFrame"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        android:clipToPadding="false"
+        android:clipChildren="false">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <com.android.setupwizardlib.view.FillContentLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_weight="1">
+
+                <!-- TODO: replace this with actual content-->
+                <ImageView
+                    style="@style/SuwContentIllustration"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:contentDescription="@null"
+                    android:src="@drawable/face_enroll_introduction" />
+
+            </com.android.setupwizardlib.view.FillContentLayout>
+
+            <TextView
+                style="@style/TextAppearance.FaceErrorText"
+                android:id="@+id/error_text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal|bottom"
+                android:accessibilityLiveRegion="polite"
+                android:gravity="center"
+                android:visibility="invisible"/>
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+</com.android.setupwizardlib.GlifLayout>
\ No newline at end of file
diff --git a/res/layout/face_enroll_enrolling_footer.xml b/res/layout/face_enroll_enrolling_footer.xml
new file mode 100644
index 0000000..e3c5872
--- /dev/null
+++ b/res/layout/face_enroll_enrolling_footer.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 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
+  -->
+
+<!-- TODO: Use aapt:attr when it is fixed (b/36809755) -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/SuwGlifButtonBar"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <Button
+        style="@style/SuwGlifButton.Secondary"
+        android:id="@+id/skip_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/security_settings_face_enroll_enrolling_skip" />
+
+</LinearLayout>
diff --git a/res/layout/face_enroll_finish.xml b/res/layout/face_enroll_finish.xml
new file mode 100644
index 0000000..9966497
--- /dev/null
+++ b/res/layout/face_enroll_finish.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 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
+  -->
+
+<com.android.setupwizardlib.GlifLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/setup_wizard_layout"
+    style="?attr/face_layout_theme"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    app:suwFooter="@layout/face_enroll_finish_footer">
+
+    <LinearLayout
+        style="@style/SuwContentFrame"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        android:clipToPadding="false"
+        android:clipChildren="false">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <com.android.setupwizardlib.view.FillContentLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_weight="1">
+
+                <ImageView
+                    style="@style/SuwContentIllustration"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:contentDescription="@null"
+                    android:src="@drawable/face_enroll_introduction" />
+
+            </com.android.setupwizardlib.view.FillContentLayout>
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+</com.android.setupwizardlib.GlifLayout>
diff --git a/res/layout/face_enroll_finish_footer.xml b/res/layout/face_enroll_finish_footer.xml
new file mode 100644
index 0000000..06d2639
--- /dev/null
+++ b/res/layout/face_enroll_finish_footer.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 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
+  -->
+
+<!-- TODO: Use aapt:attr when it is fixed (b/36809755) -->
+<com.android.setupwizardlib.view.ButtonBarLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/SuwGlifButtonBar.Stackable"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <Space
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_weight="1" />
+
+    <Button
+        style="@style/SuwGlifButton.Primary"
+        android:id="@+id/next_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/security_settings_face_enroll_done" />
+
+</com.android.setupwizardlib.view.ButtonBarLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 2d80b85..365ad28 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -897,12 +897,28 @@
     <string name="security_settings_face_enroll_introduction_message_unlock_disabled">Use you</string>
     <!-- Introduction detail message shwon in face enrollment screen in setup wizard. [CHAR LIMIT=NONE] -->
     <string name="security_settings_face_enroll_introduction_message_setup">Use your face to unlock your phone, authorize purchases, or sign in to apps</string>
+    <!-- Title shown in face enrollment dialog [CHAR LIMIT=40] -->
+    <string name="security_settings_face_enroll_repeat_title">Center your face in the circle</string>
+    <!-- Button text to skip enrollment of face [CHAR LIMIT=40] -->
+    <string name="security_settings_face_enroll_enrolling_skip">Do it later</string>
     <!-- Text shown when "Add face" button is disabled -->
     <string name="face_add_max">You can add up to <xliff:g id="count" example="5">%d</xliff:g> fingerprints</string>
     <!-- Text shown when users has enrolled a maximum number of faces [CHAR LIMIT=NONE] -->
     <string name="face_intro_error_max">You\u2019ve added the maximum number of faces</string>
     <!-- Text shown when an unknown error caused the device to be unable to add faces [CHAR LIMIT=NONE] -->
     <string name="face_intro_error_unknown">Can\u2019t add more faces</string>
+    <!-- Dialog message for dialog which shows when face cannot be enrolled. [CHAR LIMIT=45] -->
+    <string name="security_settings_face_enroll_error_dialog_title">Enrollment was not completed</string>
+    <!-- Button text shown in face dialog shown when an error occurs during enrollment [CHAR LIMIT=22] -->
+    <string name="security_settings_face_enroll_dialog_ok">OK</string>
+    <!-- Dialog message for dialog which shows when face cannot be enrolled due to being idle too long. -->
+    <string name="security_settings_face_enroll_error_timeout_dialog_message">Face enrollment time limit reached. Try again.</string>
+    <!-- Dialog message for dialog which shows when face cannot be enrolled due to an internal error or face can't be read. -->
+    <string name="security_settings_face_enroll_error_generic_dialog_message">Face enrollment didn\'t work.</string>
+    <!-- Message shown in face enrollment dialog once enrollment is completed -->
+    <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>
 
     <!-- Fingerprint enrollment and settings --><skip />
     <!-- Title shown for menu item that launches fingerprint settings or enrollment [CHAR LIMIT=22] -->
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 4e5bdda..32cdb35 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -364,6 +364,11 @@
         <item name="android:icon">@drawable/ic_fingerprint_header</item>
     </style>
 
+    <style name="TextAppearance.FaceErrorText"
+        parent="android:TextAppearance.Material.Body1">
+        <item name="android:textColor">?android:attr/colorError</item>
+    </style>
+
     <style name="FaceLayoutTheme">
         <item name="android:icon">@drawable/ic_face_header</item>
     </style>
diff --git a/src/com/android/settings/biometrics/BiometricEnrollBase.java b/src/com/android/settings/biometrics/BiometricEnrollBase.java
index c49ddd4..298891e 100644
--- a/src/com/android/settings/biometrics/BiometricEnrollBase.java
+++ b/src/com/android/settings/biometrics/BiometricEnrollBase.java
@@ -30,29 +30,33 @@
 import com.android.settings.R;
 import com.android.settings.SetupWizardUtils;
 import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling;
-import com.android.settings.biometrics.fingerprint.FingerprintSettings;
 import com.android.settings.core.InstrumentedActivity;
 import com.android.settings.password.ChooseLockSettingsHelper;
 import com.android.setupwizardlib.GlifLayout;
 
 /**
- * Base activity for all fingerprint enrollment steps.
+ * Base activity for all biometric enrollment steps.
  */
 public abstract class BiometricEnrollBase extends InstrumentedActivity
         implements View.OnClickListener {
     public static final int RESULT_FINISHED = BiometricSettings.RESULT_FINISHED;
     public static final int RESULT_SKIP = BiometricSettings.RESULT_SKIP;
     public static final int RESULT_TIMEOUT = BiometricSettings.RESULT_TIMEOUT;
+    public static final String EXTRA_KEY_LAUNCHED_CONFIRM = "launched_confirm_lock";
 
+    public static final int CONFIRM_REQUEST = 1;
+    public static final int ENROLLING = 2;
+
+    protected boolean mLaunchedConfirmLock;
     protected byte[] mToken;
     protected int mUserId;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        mToken = getIntent().getByteArrayExtra(
-                ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
+        mToken = getIntent().getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
         if (savedInstanceState != null && mToken == null) {
+            mLaunchedConfirmLock = savedInstanceState.getBoolean(EXTRA_KEY_LAUNCHED_CONFIRM);
             mToken = savedInstanceState.getByteArray(
                     ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
         }
@@ -68,6 +72,7 @@
     @Override
     protected void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
+        outState.putBoolean(EXTRA_KEY_LAUNCHED_CONFIRM, mLaunchedConfirmLock);
         outState.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken);
     }
 
@@ -77,6 +82,10 @@
         initViews();
     }
 
+    protected boolean shouldLaunchConfirmLock() {
+        return mToken == null && !mLaunchedConfirmLock;
+    }
+
     protected void initViews() {
         getWindow().setStatusBarColor(Color.TRANSPARENT);
         Button nextButton = getNextButton();
@@ -129,4 +138,25 @@
         }
         return intent;
     }
+
+    protected void launchConfirmLock(int titleResId, long challenge) {
+        ChooseLockSettingsHelper helper = new ChooseLockSettingsHelper(this);
+        boolean launchedConfirmationActivity;
+        if (mUserId == UserHandle.USER_NULL) {
+            launchedConfirmationActivity = helper.launchConfirmationActivity(CONFIRM_REQUEST,
+                    getString(titleResId),
+                    null, null, challenge);
+        } else {
+            launchedConfirmationActivity = helper.launchConfirmationActivity(CONFIRM_REQUEST,
+                    getString(titleResId),
+                    null, null, challenge, mUserId);
+        }
+        if (!launchedConfirmationActivity) {
+            // This shouldn't happen, as we should only end up at this step if a lock thingy is
+            // already set.
+            finish();
+        } else {
+            mLaunchedConfirmLock = true;
+        }
+    }
 }
diff --git a/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java b/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java
index 98775f2..beefb39 100644
--- a/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java
@@ -102,9 +102,11 @@
     protected abstract String getExtraKeyForBiometric();
 
     /**
-     * @return the intent for proceeding to the next step of enrollment
+     * @return the intent for proceeding to the next step of enrollment. For Fingerprint, this
+     * should lead to the "Find Sensor" activity. For Face, this should lead to the "Enrolling"
+     * activity.
      */
-    protected abstract Intent getFindSensorIntent();
+    protected abstract Intent getEnrollingIntent();
 
     /**
      * @param span
@@ -179,7 +181,7 @@
     }
 
     private void launchFindSensor(byte[] token) {
-        Intent intent = getFindSensorIntent();
+        Intent intent = getEnrollingIntent();
         if (token != null) {
             intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token);
         }
diff --git a/src/com/android/settings/biometrics/BiometricEnrollSidecar.java b/src/com/android/settings/biometrics/BiometricEnrollSidecar.java
new file mode 100644
index 0000000..111fecd
--- /dev/null
+++ b/src/com/android/settings/biometrics/BiometricEnrollSidecar.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2018 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.biometrics;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.UserHandle;
+
+import com.android.settings.core.InstrumentedFragment;
+import com.android.settings.password.ChooseLockSettingsHelper;
+
+import java.util.ArrayList;
+
+/**
+ * Abstract sidecar fragment to handle the state around biometric enrollment. This sidecar manages
+ * the state of enrollment throughout the activity lifecycle so the app can continue after an
+ * event like rotation.
+ */
+public abstract class BiometricEnrollSidecar extends InstrumentedFragment {
+
+    public interface Listener {
+        void onEnrollmentHelp(CharSequence helpString);
+        void onEnrollmentError(int errMsgId, CharSequence errString);
+        void onEnrollmentProgressChange(int steps, int remaining);
+    }
+
+    private int mEnrollmentSteps = -1;
+    private int mEnrollmentRemaining = 0;
+    private Listener mListener;
+    private boolean mEnrolling;
+    private Handler mHandler = new Handler();
+    private boolean mDone;
+    private ArrayList<QueuedEvent> mQueuedEvents;
+
+    protected CancellationSignal mEnrollmentCancel;
+    protected byte[] mToken;
+    protected int mUserId;
+
+    private abstract class QueuedEvent {
+        public abstract void send(Listener listener);
+    }
+
+    private class QueuedEnrollmentProgress extends QueuedEvent {
+        int enrollmentSteps;
+        int remaining;
+        public QueuedEnrollmentProgress(int enrollmentSteps, int remaining) {
+            this.enrollmentSteps = enrollmentSteps;
+            this.remaining = remaining;
+        }
+
+        @Override
+        public void send(Listener listener) {
+            listener.onEnrollmentProgressChange(enrollmentSteps, remaining);
+        }
+    }
+
+    private class QueuedEnrollmentHelp extends QueuedEvent {
+        int helpMsgId;
+        CharSequence helpString;
+        public QueuedEnrollmentHelp(int helpMsgId, CharSequence helpString) {
+            this.helpMsgId = helpMsgId;
+            this.helpString = helpString;
+        }
+
+        @Override
+        public void send(Listener listener) {
+            listener.onEnrollmentHelp(helpString);
+        }
+    }
+
+    private class QueuedEnrollmentError extends QueuedEvent {
+        int errMsgId;
+        CharSequence errString;
+        public QueuedEnrollmentError(int errMsgId, CharSequence errString) {
+            this.errMsgId = errMsgId;
+            this.errString = errString;
+        }
+
+        @Override
+        public void send(Listener listener) {
+            listener.onEnrollmentError(errMsgId, errString);
+        }
+    }
+
+    private final Runnable mTimeoutRunnable = new Runnable() {
+        @Override
+        public void run() {
+            cancelEnrollment();
+        }
+    };
+
+    public BiometricEnrollSidecar() {
+        mQueuedEvents = new ArrayList<>();
+    }
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setRetainInstance(true);
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        mToken = activity.getIntent().getByteArrayExtra(
+                ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
+        mUserId = activity.getIntent().getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_NULL);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        if (!mEnrolling) {
+            startEnrollment();
+        }
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        if (!getActivity().isChangingConfigurations()) {
+            cancelEnrollment();
+        }
+    }
+
+    protected void startEnrollment() {
+        mHandler.removeCallbacks(mTimeoutRunnable);
+        mEnrollmentSteps = -1;
+        mEnrollmentCancel = new CancellationSignal();
+        mEnrolling = true;
+    }
+
+    public boolean cancelEnrollment() {
+        mHandler.removeCallbacks(mTimeoutRunnable);
+        if (mEnrolling) {
+            mEnrollmentCancel.cancel();
+            mEnrolling = false;
+            mEnrollmentSteps = -1;
+            return true;
+        }
+        return false;
+    }
+
+    protected void onEnrollmentProgress(int remaining) {
+        if (mEnrollmentSteps == -1) {
+            mEnrollmentSteps = remaining;
+        }
+        mEnrollmentRemaining = remaining;
+        mDone = remaining == 0;
+        if (mListener != null) {
+            mListener.onEnrollmentProgressChange(mEnrollmentSteps, remaining);
+        } else {
+            mQueuedEvents.add(new QueuedEnrollmentProgress(mEnrollmentSteps, remaining));
+        }
+    }
+
+    protected void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
+        if (mListener != null) {
+            mListener.onEnrollmentHelp(helpString);
+        } else {
+            mQueuedEvents.add(new QueuedEnrollmentHelp(helpMsgId, helpString));
+        }
+    }
+
+    protected void onEnrollmentError(int errMsgId, CharSequence errString) {
+        if (mListener != null) {
+            mListener.onEnrollmentError(errMsgId, errString);
+        } else {
+            mQueuedEvents.add(new QueuedEnrollmentError(errMsgId, errString));
+        }
+        mEnrolling = false;
+    }
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+        if (mListener != null) {
+            for (int i=0; i<mQueuedEvents.size(); i++) {
+                QueuedEvent event = mQueuedEvents.get(i);
+                event.send(mListener);
+            }
+            mQueuedEvents.clear();
+        }
+    }
+
+    public int getEnrollmentSteps() {
+        return mEnrollmentSteps;
+    }
+
+    public int getEnrollmentRemaining() {
+        return mEnrollmentRemaining;
+    }
+
+    public boolean isDone() {
+        return mDone;
+    }
+
+    public boolean isEnrolling() {
+        return mEnrolling;
+    }
+}
diff --git a/src/com/android/settings/biometrics/BiometricErrorDialog.java b/src/com/android/settings/biometrics/BiometricErrorDialog.java
new file mode 100644
index 0000000..4e073f1
--- /dev/null
+++ b/src/com/android/settings/biometrics/BiometricErrorDialog.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2018 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.biometrics;
+
+import static com.android.settings.biometrics.BiometricSettings.RESULT_FINISHED;
+import static com.android.settings.biometrics.BiometricSettings.RESULT_TIMEOUT;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.hardware.biometrics.BiometricConstants;
+import android.os.Bundle;
+
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+
+/**
+ * Abstract dialog, shown when an error occurs during biometric enrollment.
+ */
+public abstract class BiometricErrorDialog extends InstrumentedDialogFragment {
+
+    public static final String KEY_ERROR_MSG = "error_msg";
+    public static final String KEY_ERROR_ID = "error_id";
+
+    public abstract int getTitleResId();
+    public abstract int getOkButtonTextResId();
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+        CharSequence errorString = getArguments().getCharSequence(KEY_ERROR_MSG);
+        final int errMsgId = getArguments().getInt(KEY_ERROR_ID);
+
+        builder.setTitle(getTitleResId())
+                .setMessage(errorString)
+                .setCancelable(false)
+                .setPositiveButton(getOkButtonTextResId(),
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int which) {
+                                dialog.dismiss();
+                                boolean wasTimeout =
+                                        errMsgId == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT;
+                                Activity activity = getActivity();
+                                activity.setResult(wasTimeout ?
+                                        RESULT_TIMEOUT : RESULT_FINISHED);
+                                activity.finish();
+                            }
+                        });
+        AlertDialog dialog = builder.create();
+        dialog.setCanceledOnTouchOutside(false);
+        return dialog;
+    }
+}
diff --git a/src/com/android/settings/biometrics/BiometricsEnrollEnrolling.java b/src/com/android/settings/biometrics/BiometricsEnrollEnrolling.java
new file mode 100644
index 0000000..ab3cd84
--- /dev/null
+++ b/src/com/android/settings/biometrics/BiometricsEnrollEnrolling.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2018 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.biometrics;
+
+import android.content.Intent;
+import android.os.UserHandle;
+import android.view.View;
+
+import com.android.settings.R;
+import com.android.settings.password.ChooseLockSettingsHelper;
+
+/**
+ * Abstract base activity which handles the actual enrolling for biometrics.
+ */
+public abstract class BiometricsEnrollEnrolling extends BiometricEnrollBase
+        implements BiometricEnrollSidecar.Listener {
+
+    private static final String TAG_SIDECAR = "sidecar";
+
+    protected BiometricEnrollSidecar mSidecar;
+
+    /**
+     * @return the intent for the finish activity
+     */
+    protected abstract Intent getFinishIntent();
+
+    /**
+     * @return an instance of the biometric enroll sidecar
+     */
+    protected abstract BiometricEnrollSidecar getSidecar();
+
+    /**
+     * @return true if enrollment should start automatically.
+     */
+    protected abstract boolean shouldStartAutomatically();
+
+    /**
+     * @return true if enrollment should finish when onStop is called.
+     */
+    protected boolean shouldFinishOnStop() {
+        return true;
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        if (shouldStartAutomatically()) {
+            startEnrollment();
+        }
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        if (mSidecar != null) {
+            mSidecar.setListener(null);
+        }
+
+        if (shouldFinishOnStop() && !isChangingConfigurations()) {
+            if (mSidecar != null) {
+                mSidecar.cancelEnrollment();
+                getFragmentManager().beginTransaction().remove(mSidecar).commitAllowingStateLoss();
+            }
+            finish();
+        }
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (mSidecar != null) {
+            mSidecar.setListener(null);
+            mSidecar.cancelEnrollment();
+            getFragmentManager().beginTransaction().remove(mSidecar).commitAllowingStateLoss();
+            mSidecar = null;
+        }
+        super.onBackPressed();
+    }
+
+    @Override
+    public void onClick(View v) {
+        switch (v.getId()) {
+            case R.id.skip_button:
+                setResult(RESULT_SKIP);
+                finish();
+                break;
+            default:
+                super.onClick(v);
+        }
+    }
+
+    public void startEnrollment() {
+        mSidecar = (BiometricEnrollSidecar) getFragmentManager().findFragmentByTag(TAG_SIDECAR);
+        if (mSidecar == null) {
+            mSidecar = getSidecar();
+            getFragmentManager().beginTransaction().add(mSidecar, TAG_SIDECAR).commit();
+        }
+        mSidecar.setListener(this);
+    }
+
+    protected void launchFinish(byte[] token) {
+        Intent intent = getFinishIntent();
+        intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT
+                | Intent.FLAG_ACTIVITY_CLEAR_TOP
+                | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+        intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token);
+        if (mUserId != UserHandle.USER_NULL) {
+            intent.putExtra(Intent.EXTRA_USER_ID, mUserId);
+        }
+        startActivity(intent);
+        overridePendingTransition(R.anim.suw_slide_next_in, R.anim.suw_slide_next_out);
+        finish();
+    }
+
+}
diff --git a/src/com/android/settings/biometrics/face/FaceEnrollEnrolling.java b/src/com/android/settings/biometrics/face/FaceEnrollEnrolling.java
new file mode 100644
index 0000000..ad92c8d
--- /dev/null
+++ b/src/com/android/settings/biometrics/face/FaceEnrollEnrolling.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2018 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.biometrics.face;
+
+import android.content.Intent;
+import android.hardware.face.FaceManager;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.settings.R;
+import com.android.settings.Utils;
+import com.android.settings.biometrics.BiometricEnrollSidecar;
+import com.android.settings.biometrics.BiometricsEnrollEnrolling;
+import com.android.settings.biometrics.BiometricErrorDialog;
+import com.android.settings.password.ChooseLockSettingsHelper;
+
+
+public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
+
+    private static final String TAG = "FaceEnrollEnrolling";
+    private static final boolean DEBUG = true;
+
+    private TextView mErrorText;
+    private Interpolator mLinearOutSlowInInterpolator;
+    private boolean mShouldFinishOnStop = true;
+
+    public static class FaceErrorDialog extends BiometricErrorDialog {
+        static FaceErrorDialog newInstance(CharSequence msg, int msgId) {
+            FaceErrorDialog dialog = new FaceErrorDialog();
+            Bundle args = new Bundle();
+            args.putCharSequence(KEY_ERROR_MSG, msg);
+            args.putInt(KEY_ERROR_ID, msgId);
+            dialog.setArguments(args);
+            return dialog;
+        }
+
+        @Override
+        public int getMetricsCategory() {
+            return MetricsProto.MetricsEvent.DIALOG_FACE_ERROR;
+        }
+
+        @Override
+        public int getTitleResId() {
+            return R.string.security_settings_face_enroll_error_dialog_title;
+        }
+
+        @Override
+        public int getOkButtonTextResId() {
+            return R.string.security_settings_face_enroll_dialog_ok;
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.face_enroll_enrolling);
+        setHeaderText(R.string.security_settings_face_enroll_repeat_title);
+        mErrorText = findViewById(R.id.error_text);
+        mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+                this, android.R.interpolator.linear_out_slow_in);
+
+        Button skipButton = findViewById(R.id.skip_button);
+        skipButton.setOnClickListener(this);
+
+        if (shouldLaunchConfirmLock()) {
+            launchConfirmLock(R.string.security_settings_face_preference_title,
+                    Utils.getFaceManagerOrNull(this).preEnroll());
+            mShouldFinishOnStop = false;
+        } else {
+            startEnrollment();
+        }
+    }
+
+    @Override
+    protected Intent getFinishIntent() {
+        return new Intent(this, FaceEnrollFinish.class);
+    }
+
+    @Override
+    protected BiometricEnrollSidecar getSidecar() {
+        return new FaceEnrollSidecar();
+    }
+
+    @Override
+    protected boolean shouldStartAutomatically() {
+        return false;
+    }
+
+    @Override
+    protected boolean shouldFinishOnStop() {
+        return mShouldFinishOnStop;
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        return MetricsProto.MetricsEvent.FACE_ENROLL_ENROLLING;
+    }
+
+    @Override
+    public void onEnrollmentHelp(CharSequence helpString) {
+        if (!TextUtils.isEmpty(helpString)) {
+            showError(helpString);
+        }
+    }
+
+    @Override
+    public void onEnrollmentError(int errMsgId, CharSequence errString) {
+        int msgId;
+        switch (errMsgId) {
+            case FaceManager.FACE_ERROR_TIMEOUT:
+                msgId = R.string.security_settings_face_enroll_error_timeout_dialog_message;
+                break;
+            default:
+                msgId = R.string.security_settings_face_enroll_error_generic_dialog_message;
+                break;
+        }
+        showErrorDialog(getText(msgId), errMsgId);
+    }
+
+    @Override
+    public void onEnrollmentProgressChange(int steps, int remaining) {
+        if (DEBUG) {
+            Log.v(TAG, "Steps: " + steps + " Remaining: " + remaining);
+        }
+        // TODO: Update the actual animation
+        showError("Steps: " + steps + " Remaining: " + remaining);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == CONFIRM_REQUEST) {
+            if (resultCode == RESULT_OK && data != null) {
+                mShouldFinishOnStop = true;
+                mToken = data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
+                overridePendingTransition(R.anim.suw_slide_next_in, R.anim.suw_slide_next_out);
+                getIntent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken);
+                startEnrollment();
+            } else {
+                finish();
+            }
+        } else {
+            super.onActivityResult(requestCode, resultCode, data);
+        }
+    }
+
+    private void showErrorDialog(CharSequence msg, int msgId) {
+        BiometricErrorDialog dialog = FaceErrorDialog.newInstance(msg, msgId);
+        dialog.show(getFragmentManager(), FaceErrorDialog.class.getName());
+    }
+
+    private void showError(CharSequence error) {
+        mErrorText.setText(error);
+        if (mErrorText.getVisibility() == View.INVISIBLE) {
+            mErrorText.setVisibility(View.VISIBLE);
+            mErrorText.setTranslationY(getResources().getDimensionPixelSize(
+                    R.dimen.fingerprint_error_text_appear_distance));
+            mErrorText.setAlpha(0f);
+            mErrorText.animate()
+                    .alpha(1f)
+                    .translationY(0f)
+                    .setDuration(200)
+                    .setInterpolator(mLinearOutSlowInInterpolator)
+                    .start();
+        } else {
+            mErrorText.animate().cancel();
+            mErrorText.setAlpha(1f);
+            mErrorText.setTranslationY(0f);
+        }
+    }
+}
diff --git a/src/com/android/settings/biometrics/face/FaceEnrollFinish.java b/src/com/android/settings/biometrics/face/FaceEnrollFinish.java
new file mode 100644
index 0000000..7062fe3
--- /dev/null
+++ b/src/com/android/settings/biometrics/face/FaceEnrollFinish.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2018 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.biometrics.face;
+
+import android.os.Bundle;
+
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.settings.R;
+import com.android.settings.biometrics.BiometricEnrollBase;
+
+/**
+ * Activity which concludes face enrollment.
+ */
+public class FaceEnrollFinish extends BiometricEnrollBase {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.face_enroll_finish);
+        setHeaderText(R.string.security_settings_face_enroll_finish_title);
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        return MetricsProto.MetricsEvent.FACE_ENROLL_FINISHED;
+    }
+
+    @Override
+    public void onNextButtonClick() {
+        setResult(RESULT_FINISHED);
+        finish();
+    }
+}
diff --git a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
index b4a33f3..cd04b22 100644
--- a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
@@ -113,8 +113,8 @@
     }
 
     @Override
-    protected Intent getFindSensorIntent() {
-        return null; // TODO
+    protected Intent getEnrollingIntent() {
+        return new Intent(this, FaceEnrollEnrolling.class);
     }
 
     @Override
diff --git a/src/com/android/settings/biometrics/face/FaceEnrollSidecar.java b/src/com/android/settings/biometrics/face/FaceEnrollSidecar.java
new file mode 100644
index 0000000..7b445e5
--- /dev/null
+++ b/src/com/android/settings/biometrics/face/FaceEnrollSidecar.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2018 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.biometrics.face;
+
+import android.app.Activity;
+import android.hardware.face.FaceManager;
+import android.os.UserHandle;
+
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.settings.Utils;
+import com.android.settings.biometrics.BiometricEnrollSidecar;
+
+/**
+ * Sidecar fragment to handle the state around face enrollment
+ */
+public class FaceEnrollSidecar extends BiometricEnrollSidecar {
+
+    private FaceManager mFaceManager;
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        mFaceManager = Utils.getFaceManagerOrNull(activity);
+    }
+
+    @Override
+    public void startEnrollment() {
+        super.startEnrollment();
+        if (mUserId != UserHandle.USER_NULL) {
+            mFaceManager.setActiveUser(mUserId);
+        }
+        mFaceManager.enroll(mToken, mEnrollmentCancel,
+                0 /* flags */, mUserId, mEnrollmentCallback);
+    }
+
+    private FaceManager.EnrollmentCallback mEnrollmentCallback
+            = new FaceManager.EnrollmentCallback() {
+
+        @Override
+        public void onEnrollmentProgress(int remaining) {
+            FaceEnrollSidecar.super.onEnrollmentProgress(remaining);
+        }
+
+        @Override
+        public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
+            FaceEnrollSidecar.super.onEnrollmentHelp(helpMsgId, helpString);
+        }
+
+        @Override
+        public void onEnrollmentError(int errMsgId, CharSequence errString) {
+            FaceEnrollSidecar.super.onEnrollmentError(errMsgId, errString);
+        }
+    };
+
+    @Override
+    public int getMetricsCategory() {
+        return MetricsProto.MetricsEvent.FACE_ENROLL_SIDECAR;
+    }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
index 9b0d1a6..3d4c5d4 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
@@ -18,7 +18,6 @@
 
 import android.animation.Animator;
 import android.animation.ObjectAnimator;
-import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.content.DialogInterface;
@@ -30,7 +29,6 @@
 import android.hardware.fingerprint.FingerprintManager;
 import android.media.AudioAttributes;
 import android.os.Bundle;
-import android.os.UserHandle;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.text.TextUtils;
@@ -44,15 +42,15 @@
 
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.settings.R;
-import com.android.settings.biometrics.BiometricEnrollBase;
+import com.android.settings.biometrics.BiometricEnrollSidecar;
+import com.android.settings.biometrics.BiometricErrorDialog;
+import com.android.settings.biometrics.BiometricsEnrollEnrolling;
 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
-import com.android.settings.password.ChooseLockSettingsHelper;
 
 /**
  * Activity which handles the actual enrolling for fingerprint.
  */
-public class FingerprintEnrollEnrolling extends BiometricEnrollBase
-        implements FingerprintEnrollSidecar.Listener {
+public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
 
     static final String TAG_SIDECAR = "sidecar";
 
@@ -93,13 +91,38 @@
     private Interpolator mLinearOutSlowInInterpolator;
     private Interpolator mFastOutLinearInInterpolator;
     private int mIconTouchCount;
-    private FingerprintEnrollSidecar mSidecar;
     private boolean mAnimationCancelled;
     private AnimatedVectorDrawable mIconAnimationDrawable;
     private AnimatedVectorDrawable mIconBackgroundBlinksDrawable;
     private boolean mRestoring;
     private Vibrator mVibrator;
 
+    public static class FingerprintErrorDialog extends BiometricErrorDialog {
+        static FingerprintErrorDialog newInstance(CharSequence msg, int msgId) {
+            FingerprintErrorDialog dialog = new FingerprintErrorDialog();
+            Bundle args = new Bundle();
+            args.putCharSequence(KEY_ERROR_MSG, msg);
+            args.putInt(KEY_ERROR_ID, msgId);
+            dialog.setArguments(args);
+            return dialog;
+        }
+
+        @Override
+        public int getMetricsCategory() {
+            return MetricsEvent.DIALOG_FINGERPINT_ERROR;
+        }
+
+        @Override
+        public int getTitleResId() {
+            return R.string.security_settings_fingerprint_enroll_error_dialog_title;
+        }
+
+        @Override
+        public int getOkButtonTextResId() {
+            return R.string.security_settings_fingerprint_enroll_dialog_ok;
+        }
+    }
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -148,14 +171,18 @@
     }
 
     @Override
+    protected BiometricEnrollSidecar getSidecar() {
+        return new FingerprintEnrollSidecar();
+    }
+
+    @Override
+    protected boolean shouldStartAutomatically() {
+        return true;
+    }
+
+    @Override
     protected void onStart() {
         super.onStart();
-        mSidecar = (FingerprintEnrollSidecar) getFragmentManager().findFragmentByTag(TAG_SIDECAR);
-        if (mSidecar == null) {
-            mSidecar = new FingerprintEnrollSidecar();
-            getFragmentManager().beginTransaction().add(mSidecar, TAG_SIDECAR).commit();
-        }
-        mSidecar.setListener(this);
         updateProgress(false /* animate */);
         updateDescription();
         if (mRestoring) {
@@ -182,40 +209,7 @@
     @Override
     protected void onStop() {
         super.onStop();
-        if (mSidecar != null) {
-            mSidecar.setListener(null);
-        }
         stopIconAnimation();
-        if (!isChangingConfigurations()) {
-            if (mSidecar != null) {
-                mSidecar.cancelEnrollment();
-                getFragmentManager().beginTransaction().remove(mSidecar).commitAllowingStateLoss();
-            }
-            finish();
-        }
-    }
-
-    @Override
-    public void onBackPressed() {
-        if (mSidecar != null) {
-            mSidecar.setListener(null);
-            mSidecar.cancelEnrollment();
-            getFragmentManager().beginTransaction().remove(mSidecar).commitAllowingStateLoss();
-            mSidecar = null;
-        }
-        super.onBackPressed();
-    }
-
-    @Override
-    public void onClick(View v) {
-        switch (v.getId()) {
-            case R.id.skip_button:
-                setResult(RESULT_SKIP);
-                finish();
-                break;
-            default:
-                super.onClick(v);
-        }
     }
 
     private void animateProgress(int progress) {
@@ -235,20 +229,6 @@
         mIconBackgroundBlinksDrawable.start();
     }
 
-    private void launchFinish(byte[] token) {
-        Intent intent = getFinishIntent();
-        intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT
-                | Intent.FLAG_ACTIVITY_CLEAR_TOP
-                | Intent.FLAG_ACTIVITY_SINGLE_TOP);
-        intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token);
-        if (mUserId != UserHandle.USER_NULL) {
-            intent.putExtra(Intent.EXTRA_USER_ID, mUserId);
-        }
-        startActivity(intent);
-        overridePendingTransition(R.anim.suw_slide_next_in, R.anim.suw_slide_next_out);
-        finish();
-    }
-
     protected Intent getFinishIntent() {
         return new Intent(this, FingerprintEnrollFinish.class);
     }
@@ -263,7 +243,6 @@
         }
     }
 
-
     @Override
     public void onEnrollmentHelp(CharSequence helpString) {
         if (!TextUtils.isEmpty(helpString)) {
@@ -323,8 +302,8 @@
     }
 
     private void showErrorDialog(CharSequence msg, int msgId) {
-        ErrorDialog dlg = ErrorDialog.newInstance(msg, msgId);
-        dlg.show(getFragmentManager(), ErrorDialog.class.getName());
+        BiometricErrorDialog dlg = FingerprintErrorDialog.newInstance(msg, msgId);
+        dlg.show(getFragmentManager(), FingerprintErrorDialog.class.getName());
     }
 
     private void showIconTouchDialog() {
@@ -455,54 +434,4 @@
             return MetricsEvent.DIALOG_FINGERPRINT_ICON_TOUCH;
         }
     }
-
-    public static class ErrorDialog extends InstrumentedDialogFragment {
-
-        /**
-         * Create a new instance of ErrorDialog.
-         *
-         * @param msg the string to show for message text
-         * @param msgId the FingerprintManager error id so we know the cause
-         * @return a new ErrorDialog
-         */
-        static ErrorDialog newInstance(CharSequence msg, int msgId) {
-            ErrorDialog dlg = new ErrorDialog();
-            Bundle args = new Bundle();
-            args.putCharSequence("error_msg", msg);
-            args.putInt("error_id", msgId);
-            dlg.setArguments(args);
-            return dlg;
-        }
-
-        @Override
-        public Dialog onCreateDialog(Bundle savedInstanceState) {
-            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
-            CharSequence errorString = getArguments().getCharSequence("error_msg");
-            final int errMsgId = getArguments().getInt("error_id");
-            builder.setTitle(R.string.security_settings_fingerprint_enroll_error_dialog_title)
-                    .setMessage(errorString)
-                    .setCancelable(false)
-                    .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok,
-                            new DialogInterface.OnClickListener() {
-                                @Override
-                                public void onClick(DialogInterface dialog, int which) {
-                                    dialog.dismiss();
-                                    boolean wasTimeout =
-                                        errMsgId == FingerprintManager.FINGERPRINT_ERROR_TIMEOUT;
-                                    Activity activity = getActivity();
-                                    activity.setResult(wasTimeout ?
-                                            RESULT_TIMEOUT : RESULT_FINISHED);
-                                    activity.finish();
-                                }
-                            });
-            AlertDialog dialog = builder.create();
-            dialog.setCanceledOnTouchOutside(false);
-            return dialog;
-        }
-
-        @Override
-        public int getMetricsCategory() {
-            return MetricsEvent.DIALOG_FINGERPINT_ERROR;
-        }
-    }
 }
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
index 047dda8..93a8d6e 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
@@ -19,16 +19,14 @@
 import android.content.Intent;
 import android.hardware.fingerprint.FingerprintManager;
 import android.os.Bundle;
-import android.os.UserHandle;
 import android.view.View;
 import android.widget.Button;
 
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.settings.R;
 import com.android.settings.Utils;
 import com.android.settings.biometrics.BiometricEnrollBase;
-import com.android.settings.biometrics.fingerprint.FingerprintEnrollSidecar.Listener;
+import com.android.settings.biometrics.BiometricEnrollSidecar.Listener;
 import com.android.settings.password.ChooseLockSettingsHelper;
 
 import androidx.annotation.Nullable;
@@ -38,14 +36,9 @@
  */
 public class FingerprintEnrollFindSensor extends BiometricEnrollBase {
 
-    @VisibleForTesting
-    static final int CONFIRM_REQUEST = 1;
-    private static final int ENROLLING = 2;
-    public static final String EXTRA_KEY_LAUNCHED_CONFIRM = "launched_confirm_lock";
-
     @Nullable
     private FingerprintFindSensorAnimation mAnimation;
-    private boolean mLaunchedConfirmLock;
+
     private FingerprintEnrollSidecar mSidecar;
     private boolean mNextClicked;
 
@@ -57,13 +50,10 @@
         skipButton.setOnClickListener(this);
 
         setHeaderText(R.string.security_settings_fingerprint_enroll_find_sensor_title);
-        if (savedInstanceState != null) {
-            mLaunchedConfirmLock = savedInstanceState.getBoolean(EXTRA_KEY_LAUNCHED_CONFIRM);
-            mToken = savedInstanceState.getByteArray(
-                    ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
-        }
-        if (mToken == null && !mLaunchedConfirmLock) {
-            launchConfirmLock();
+
+        if (shouldLaunchConfirmLock()) {
+            launchConfirmLock(R.string.security_settings_fingerprint_preference_title,
+                    Utils.getFingerprintManagerOrNull(this).preEnroll());
         } else if (mToken != null) {
             startLookingForFingerprint(); // already confirmed, so start looking for fingerprint
         }
@@ -133,13 +123,6 @@
     }
 
     @Override
-    public void onSaveInstanceState(Bundle outState) {
-        super.onSaveInstanceState(outState);
-        outState.putBoolean(EXTRA_KEY_LAUNCHED_CONFIRM, mLaunchedConfirmLock);
-        outState.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken);
-    }
-
-    @Override
     public void onClick(View v) {
         switch (v.getId()) {
             case R.id.skip_button:
@@ -209,28 +192,6 @@
         }
     }
 
-    private void launchConfirmLock() {
-        long challenge = Utils.getFingerprintManagerOrNull(this).preEnroll();
-        ChooseLockSettingsHelper helper = new ChooseLockSettingsHelper(this);
-        boolean launchedConfirmationActivity = false;
-        if (mUserId == UserHandle.USER_NULL) {
-            launchedConfirmationActivity = helper.launchConfirmationActivity(CONFIRM_REQUEST,
-                getString(R.string.security_settings_fingerprint_preference_title),
-                null, null, challenge);
-        } else {
-            launchedConfirmationActivity = helper.launchConfirmationActivity(CONFIRM_REQUEST,
-                    getString(R.string.security_settings_fingerprint_preference_title),
-                    null, null, challenge, mUserId);
-        }
-        if (!launchedConfirmationActivity) {
-            // This shouldn't happen, as we should only end up at this step if a lock thingy is
-            // already set.
-            finish();
-        } else {
-            mLaunchedConfirmLock = true;
-        }
-    }
-
     @Override
     public int getMetricsCategory() {
         return MetricsEvent.FINGERPRINT_FIND_SENSOR;
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
index ed111f4..41bf86f 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
@@ -117,7 +117,7 @@
     }
 
     @Override
-    protected Intent getFindSensorIntent() {
+    protected Intent getEnrollingIntent() {
         return new Intent(this, FingerprintEnrollFindSensor.class);
     }
 
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java
index af56310..27d71cd 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java
@@ -16,164 +16,35 @@
 
 package com.android.settings.biometrics.fingerprint;
 
-import android.annotation.Nullable;
 import android.app.Activity;
-import android.content.Intent;
 import android.hardware.fingerprint.FingerprintManager;
-import android.os.Bundle;
-import android.os.CancellationSignal;
-import android.os.Handler;
 import android.os.UserHandle;
 
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.settings.Utils;
-import com.android.settings.core.InstrumentedFragment;
-import com.android.settings.password.ChooseLockSettingsHelper;
-
-import java.util.ArrayList;
+import com.android.settings.biometrics.BiometricEnrollSidecar;
 
 /**
  * Sidecar fragment to handle the state around fingerprint enrollment.
  */
-public class FingerprintEnrollSidecar extends InstrumentedFragment {
+public class FingerprintEnrollSidecar extends BiometricEnrollSidecar {
 
-    private int mEnrollmentSteps = -1;
-    private int mEnrollmentRemaining = 0;
-    private Listener mListener;
-    private boolean mEnrolling;
-    private CancellationSignal mEnrollmentCancel;
-    private Handler mHandler = new Handler();
-    private byte[] mToken;
-    private boolean mDone;
-    private int mUserId;
     private FingerprintManager mFingerprintManager;
-    private ArrayList<QueuedEvent> mQueuedEvents;
-
-    private abstract class QueuedEvent {
-        public abstract void send(Listener listener);
-    }
-
-    private class QueuedEnrollmentProgress extends QueuedEvent {
-        int enrollmentSteps;
-        int remaining;
-        public QueuedEnrollmentProgress(int enrollmentSteps, int remaining) {
-            this.enrollmentSteps = enrollmentSteps;
-            this.remaining = remaining;
-        }
-
-        @Override
-        public void send(Listener listener) {
-            listener.onEnrollmentProgressChange(enrollmentSteps, remaining);
-        }
-    }
-
-    private class QueuedEnrollmentHelp extends QueuedEvent {
-        int helpMsgId;
-        CharSequence helpString;
-        public QueuedEnrollmentHelp(int helpMsgId, CharSequence helpString) {
-            this.helpMsgId = helpMsgId;
-            this.helpString = helpString;
-        }
-
-        @Override
-        public void send(Listener listener) {
-            listener.onEnrollmentHelp(helpString);
-        }
-    }
-
-    private class QueuedEnrollmentError extends QueuedEvent {
-        int errMsgId;
-        CharSequence errString;
-        public QueuedEnrollmentError(int errMsgId, CharSequence errString) {
-            this.errMsgId = errMsgId;
-            this.errString = errString;
-        }
-
-        @Override
-        public void send(Listener listener) {
-            listener.onEnrollmentError(errMsgId, errString);
-        }
-    }
-
-    public FingerprintEnrollSidecar() {
-        mQueuedEvents = new ArrayList<>();
-    }
-
-    @Override
-    public void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setRetainInstance(true);
-    }
 
     @Override
     public void onAttach(Activity activity) {
         super.onAttach(activity);
         mFingerprintManager = Utils.getFingerprintManagerOrNull(activity);
-        mToken = activity.getIntent().getByteArrayExtra(
-                ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
-        mUserId = activity.getIntent().getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_NULL);
     }
 
     @Override
-    public void onStart() {
-        super.onStart();
-        if (!mEnrolling) {
-            startEnrollment();
-        }
-    }
-
-    @Override
-    public void onStop() {
-        super.onStop();
-        if (!getActivity().isChangingConfigurations()) {
-            cancelEnrollment();
-        }
-    }
-
-    private void startEnrollment() {
-        mHandler.removeCallbacks(mTimeoutRunnable);
-        mEnrollmentSteps = -1;
-        mEnrollmentCancel = new CancellationSignal();
+    protected void startEnrollment() {
+        super.startEnrollment();
         if (mUserId != UserHandle.USER_NULL) {
             mFingerprintManager.setActiveUser(mUserId);
         }
         mFingerprintManager.enroll(mToken, mEnrollmentCancel,
                 0 /* flags */, mUserId, mEnrollmentCallback);
-        mEnrolling = true;
-    }
-
-    boolean cancelEnrollment() {
-        mHandler.removeCallbacks(mTimeoutRunnable);
-        if (mEnrolling) {
-            mEnrollmentCancel.cancel();
-            mEnrolling = false;
-            mEnrollmentSteps = -1;
-            return true;
-        }
-        return false;
-    }
-
-    public void setListener(Listener listener) {
-        mListener = listener;
-        if (mListener != null) {
-            for (int i=0; i<mQueuedEvents.size(); i++) {
-                QueuedEvent event = mQueuedEvents.get(i);
-                event.send(mListener);
-            }
-            mQueuedEvents.clear();
-        }
-    }
-
-    public int getEnrollmentSteps() {
-        return mEnrollmentSteps;
-    }
-
-    public int getEnrollmentRemaining() {
-        return mEnrollmentRemaining;
-    }
-
-    public boolean isDone() {
-        return mDone;
     }
 
     private FingerprintManager.EnrollmentCallback mEnrollmentCallback
@@ -181,42 +52,17 @@
 
         @Override
         public void onEnrollmentProgress(int remaining) {
-            if (mEnrollmentSteps == -1) {
-                mEnrollmentSteps = remaining;
-            }
-            mEnrollmentRemaining = remaining;
-            mDone = remaining == 0;
-            if (mListener != null) {
-                mListener.onEnrollmentProgressChange(mEnrollmentSteps, remaining);
-            } else {
-                mQueuedEvents.add(new QueuedEnrollmentProgress(mEnrollmentSteps, remaining));
-            }
+            FingerprintEnrollSidecar.super.onEnrollmentProgress(remaining);
         }
 
         @Override
         public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
-            if (mListener != null) {
-                mListener.onEnrollmentHelp(helpString);
-            } else {
-                mQueuedEvents.add(new QueuedEnrollmentHelp(helpMsgId, helpString));
-            }
+            FingerprintEnrollSidecar.super.onEnrollmentHelp(helpMsgId, helpString);
         }
 
         @Override
         public void onEnrollmentError(int errMsgId, CharSequence errString) {
-            if (mListener != null) {
-                mListener.onEnrollmentError(errMsgId, errString);
-            } else {
-                mQueuedEvents.add(new QueuedEnrollmentError(errMsgId, errString));
-            }
-            mEnrolling = false;
-        }
-    };
-
-    private final Runnable mTimeoutRunnable = new Runnable() {
-        @Override
-        public void run() {
-            cancelEnrollment();
+            FingerprintEnrollSidecar.super.onEnrollmentError(errMsgId, errString);
         }
     };
 
@@ -224,14 +70,4 @@
     public int getMetricsCategory() {
         return MetricsEvent.FINGERPRINT_ENROLL_SIDECAR;
     }
-
-    public interface Listener {
-        void onEnrollmentHelp(CharSequence helpString);
-        void onEnrollmentError(int errMsgId, CharSequence errString);
-        void onEnrollmentProgressChange(int steps, int remaining);
-    }
-
-    public boolean isEnrolling() {
-        return mEnrolling;
-    }
 }
diff --git a/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java b/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java
index 9fcbbf5..6ffc096 100644
--- a/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java
@@ -70,7 +70,7 @@
     }
 
     @Override
-    protected Intent getFindSensorIntent() {
+    protected Intent getEnrollingIntent() {
         final Intent intent = new Intent(this, SetupFingerprintEnrollFindSensor.class);
         SetupWizardUtils.copySetupExtras(getIntent(), intent);
         return intent;
diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensorTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensorTest.java
index 70a68ec..147ed1d 100644
--- a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensorTest.java
+++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensorTest.java
@@ -157,7 +157,7 @@
     @Test
     public void onActivityResult_withNullIntentShouldNotCrash() {
         // this should not crash
-        mActivity.onActivityResult(FingerprintEnrollFindSensor.CONFIRM_REQUEST, Activity.RESULT_OK,
+        mActivity.onActivityResult(BiometricEnrollBase.CONFIRM_REQUEST, Activity.RESULT_OK,
             null);
         assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(Activity.RESULT_CANCELED);
     }