Add plumbing and placeholder screens for parental consent flow.

Bug: 188847063
Test: adb shell am start -a android.settings.BIOMETRIC_ENROLL --ez require_consent true
Test: atest com.android.settings.biometrics.ParentalConsentHelperTest
Change-Id: Ie136036d5f550775fd0b021979581a5d222f1b68
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 65a6f04..6f6482d 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1803,6 +1803,10 @@
             android:theme="@style/GlifV3Theme.Light"
             android:exported="false"/>
 
+        <activity android:name=".biometrics.face.FaceEnrollParentalConsent"
+                  android:exported="false"
+                  android:screenOrientation="portrait"/>
+
         <activity android:name=".biometrics.face.FaceEnrollIntroduction"
             android:exported="false"
             android:screenOrientation="portrait"/>
@@ -1837,6 +1841,7 @@
         <activity android:name=".biometrics.fingerprint.FingerprintEnrollFindSensor" android:exported="false"/>
         <activity android:name=".biometrics.fingerprint.FingerprintEnrollEnrolling" android:exported="false"/>
         <activity android:name=".biometrics.fingerprint.FingerprintEnrollFinish" android:exported="false"/>
+        <activity android:name=".biometrics.fingerprint.FingerprintEnrollParentalConsent" android:exported="false"/>
         <activity android:name=".biometrics.fingerprint.FingerprintEnrollIntroduction"
             android:exported="true"
             android:theme="@style/GlifTheme.Light">
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3b4fae4..0302461 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -777,6 +777,8 @@
     <string name="security_settings_face_enroll_introduction_more">More</string>
     <!-- Introduction title shown in face enrollment to introduce the face unlock feature [CHAR LIMIT=40] -->
     <string name="security_settings_face_enroll_introduction_title">Unlock with your face</string>
+    <!-- Introduction title shown in face enrollment when when asking for parental consent for the face unlock feature [CHAR LIMIT=40] -->
+    <string name="security_settings_face_enroll_consent_introduction_title">Allow face unlock</string>
     <!-- Introduction title shown in face enrollment to introduce the face unlock feature, when face unlock is disabled by device admin [CHAR LIMIT=60] -->
     <string name="security_settings_face_enroll_introduction_title_unlock_disabled">Use your face to authenticate</string>
     <!-- Introduction detail message shown in face enrollment dialog [CHAR LIMIT=NONE]-->
@@ -888,8 +890,10 @@
     </plurals>
     <!-- message shown in summary field when no fingerprints are registered -->
     <string name="security_settings_fingerprint_preference_summary_none"></string>
-    <!-- Introduction title shown in fingerprint enrollment to introduce the fingerprint feature[CHAR LIMIT=29] -->
+    <!-- Introduction title shown in fingerprint enrollment to introduce the fingerprint feature [CHAR LIMIT=29] -->
     <string name="security_settings_fingerprint_enroll_introduction_title">Set up your fingerprint</string>
+    <!-- Introduction title shown in fingerprint enrollment when asking for parental consent for fingerprint unlock [CHAR LIMIT=29] -->
+    <string name="security_settings_fingerprint_enroll_consent_introduction_title">Allow fingerprint unlock</string>
     <!-- Introduction title shown in fingerprint enrollment to introduce the fingerprint feature, when fingerprint unlock is disabled by device admin [CHAR LIMIT=40] -->
     <string name="security_settings_fingerprint_enroll_introduction_title_unlock_disabled">Use your fingerprint</string>
     <!-- Introduction detail message shown in fingerprint enrollment dialog [CHAR LIMIT=NONE]-->
diff --git a/src/com/android/settings/biometrics/BiometricEnrollActivity.java b/src/com/android/settings/biometrics/BiometricEnrollActivity.java
index ef01a4b..6ab9ab8 100644
--- a/src/com/android/settings/biometrics/BiometricEnrollActivity.java
+++ b/src/com/android/settings/biometrics/BiometricEnrollActivity.java
@@ -19,6 +19,9 @@
 import static android.provider.Settings.ACTION_BIOMETRIC_ENROLL;
 import static android.provider.Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED;
 
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_CONSENT_DENIED;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_CONSENT_GRANTED;
+
 import android.annotation.NonNull;
 import android.app.admin.DevicePolicyManager;
 import android.app.settings.SettingsEnums;
@@ -64,6 +67,8 @@
 
     private static final int REQUEST_CHOOSE_LOCK = 1;
     private static final int REQUEST_CONFIRM_LOCK = 2;
+    // prompt for parental consent options
+    private static final int REQUEST_CHOOSE_OPTIONS = 3;
 
     public static final int RESULT_SKIP = BiometricEnrollBase.RESULT_SKIP;
 
@@ -71,8 +76,12 @@
     // this only applies to fingerprint.
     public static final String EXTRA_SKIP_INTRO = "skip_intro";
 
+    // TODO: temporary while waiting for team to add real flag
+    public static final String EXTRA_TEMP_REQUIRE_PARENTAL_CONSENT = "require_consent";
+
     private static final String SAVED_STATE_CONFIRMING_CREDENTIALS = "confirming_credentials";
     private static final String SAVED_STATE_ENROLL_ACTION_LOGGED = "enroll_action_logged";
+    private static final String SAVED_STATE_PARENTAL_OPTIONS = "enroll_preferences";
     private static final String SAVED_STATE_GK_PW_HANDLE = "gk_pw_handle";
 
     public static final class InternalActivity extends BiometricEnrollActivity {}
@@ -80,9 +89,14 @@
     private int mUserId = UserHandle.myUserId();
     private boolean mConfirmingCredentials;
     private boolean mIsEnrollActionLogged;
-    private boolean mIsFaceEnrollable;
-    private boolean mIsFingerprintEnrollable;
+    private boolean mHasFeatureFace = false;
+    private boolean mHasFeatureFingerprint = false;
+    private boolean mIsFaceEnrollable = false;
+    private boolean mIsFingerprintEnrollable = false;
+    private boolean mParentalOptionsRequired = false;
+    private Bundle mParentalOptions;
     @Nullable private Long mGkPwHandle;
+    @Nullable private ParentalConsentHelper mParentalConsentHelper;
     @Nullable private MultiBiometricEnrollHelper mMultiBiometricEnrollHelper;
 
     @Override
@@ -101,6 +115,7 @@
                     SAVED_STATE_CONFIRMING_CREDENTIALS, false);
             mIsEnrollActionLogged = savedInstanceState.getBoolean(
                     SAVED_STATE_ENROLL_ACTION_LOGGED, false);
+            mParentalOptions = savedInstanceState.getBundle(SAVED_STATE_PARENTAL_OPTIONS);
             if (savedInstanceState.containsKey(SAVED_STATE_GK_PW_HANDLE)) {
                 mGkPwHandle = savedInstanceState.getLong(SAVED_STATE_GK_PW_HANDLE);
             }
@@ -141,52 +156,98 @@
                     SetupWizardUtils.getThemeString(intent));
         }
 
-        // Default behavior is to enroll BIOMETRIC_WEAK or above. See ACTION_BIOMETRIC_ENROLL.
-        final int authenticators = intent.getIntExtra(
-                EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, Authenticators.BIOMETRIC_WEAK);
+        final PackageManager pm = getApplicationContext().getPackageManager();
+        mHasFeatureFingerprint = pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT);
+        mHasFeatureFace = pm.hasSystemFeature(PackageManager.FEATURE_FACE);
 
+        // determine what can be enrolled
+        final boolean isSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent());
+        if (mHasFeatureFace) {
+            final FaceManager faceManager = getSystemService(FaceManager.class);
+            final List<FaceSensorPropertiesInternal> faceProperties =
+                    faceManager.getSensorPropertiesInternal();
+            if (!faceProperties.isEmpty()) {
+                final int maxEnrolls =
+                        isSetupWizard ? 1 : faceProperties.get(0).maxEnrollmentsPerUser;
+                mIsFaceEnrollable =
+                        faceManager.getEnrolledFaces(mUserId).size() < maxEnrolls;
+            }
+        }
+        if (mHasFeatureFingerprint) {
+            final FingerprintManager fpManager = getSystemService(FingerprintManager.class);
+            final List<FingerprintSensorPropertiesInternal> fpProperties =
+                    fpManager.getSensorPropertiesInternal();
+            if (!fpProperties.isEmpty()) {
+                final int maxEnrolls =
+                        isSetupWizard ? 1 : fpProperties.get(0).maxEnrollmentsPerUser;
+                mIsFingerprintEnrollable =
+                        fpManager.getEnrolledFingerprints(mUserId).size() < maxEnrolls;
+            }
+        }
+
+        // TODO(b/188847063): replace with real flag when ready
+        mParentalOptionsRequired = intent.getBooleanExtra(
+                BiometricEnrollActivity.EXTRA_TEMP_REQUIRE_PARENTAL_CONSENT, false);
+
+        if (mParentalOptionsRequired && mParentalOptions == null) {
+            mParentalConsentHelper = new ParentalConsentHelper(
+                    mIsFaceEnrollable, mIsFingerprintEnrollable, mGkPwHandle);
+            setOrConfirmCredentialsNow();
+        } else {
+            startEnroll();
+        }
+    }
+
+    private void startEnroll() {
+        // TODO(b/188847063): This can be deleted, but log it now until it's wired up for real.
+        if (mParentalOptionsRequired) {
+            if (mParentalOptions == null) {
+                throw new IllegalStateException("consent options required, but not set");
+            }
+            Log.d(TAG, "consent for face: "
+                    + ParentalConsentHelper.hasFaceConsent(mParentalOptions));
+            Log.d(TAG, "consent for fingerprint: "
+                    + ParentalConsentHelper.hasFingerprintConsent(mParentalOptions));
+        } else {
+            Log.d(TAG, "startEnroll without requiring consent");
+        }
+
+        // Default behavior is to enroll BIOMETRIC_WEAK or above. See ACTION_BIOMETRIC_ENROLL.
+        final int authenticators = getIntent().getIntExtra(
+                EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, Authenticators.BIOMETRIC_WEAK);
         Log.d(TAG, "Authenticators: " + authenticators);
 
-        final PackageManager pm = getApplicationContext().getPackageManager();
-        final boolean hasFeatureFingerprint =
-                pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT);
-        final boolean hasFeatureFace = pm.hasSystemFeature(PackageManager.FEATURE_FACE);
-        final boolean isSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent());
+        startEnrollWith(authenticators, WizardManagerHelper.isAnySetupWizard(getIntent()));
+    }
 
-        if (isSetupWizard) {
-            if (hasFeatureFace && hasFeatureFingerprint) {
-                setupForMultiBiometricEnroll();
-            } else if (hasFeatureFace) {
-                launchFaceOnlyEnroll();
-            } else if (hasFeatureFingerprint) {
-                launchFingerprintOnlyEnroll();
-            } else {
-                Log.e(TAG, "No biometrics but started by SUW?");
-                finish();
-            }
-        } else {
-            // If the caller is not setup wizard, and the user has something enrolled, finish.
+    private void startEnrollWith(@Authenticators.Types int authenticators, boolean setupWizard) {
+        // If the caller is not setup wizard, and the user has something enrolled, finish.
+        if (!setupWizard) {
             final BiometricManager bm = getSystemService(BiometricManager.class);
             final @BiometricError int result = bm.canAuthenticate(authenticators);
             if (result != BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
-                Log.e(TAG, "Unexpected result: " + result);
+                Log.e(TAG, "Unexpected result (has enrollments): " + result);
                 finish();
                 return;
             }
+        }
 
-            // This will need to be updated if the device has sensors other than BIOMETRIC_STRONG
-            if (authenticators == BiometricManager.Authenticators.DEVICE_CREDENTIAL) {
-                launchCredentialOnlyEnroll();
-            } else if (hasFeatureFace && hasFeatureFingerprint) {
-                setupForMultiBiometricEnroll();
-            } else if (hasFeatureFingerprint) {
-                launchFingerprintOnlyEnroll();
-            } else if (hasFeatureFace) {
-                launchFaceOnlyEnroll();
+        // This will need to be updated if the device has sensors other than BIOMETRIC_STRONG
+        if (!setupWizard && authenticators == BiometricManager.Authenticators.DEVICE_CREDENTIAL) {
+            launchCredentialOnlyEnroll();
+        } else if (mHasFeatureFace && mHasFeatureFingerprint) {
+            if (mParentalOptionsRequired && mGkPwHandle != null) {
+                launchFaceAndFingerprintEnroll();
             } else {
-                Log.e(TAG, "Unknown state, finishing");
-                finish();
+                setOrConfirmCredentialsNow();
             }
+        } else if (mHasFeatureFingerprint) {
+            launchFingerprintOnlyEnroll();
+        } else if (mHasFeatureFace) {
+            launchFaceOnlyEnroll();
+        } else {
+            Log.e(TAG, "Unknown state, finishing (was SUW: " + setupWizard + ")");
+            finish();
         }
     }
 
@@ -195,6 +256,9 @@
         super.onSaveInstanceState(outState);
         outState.putBoolean(SAVED_STATE_CONFIRMING_CREDENTIALS, mConfirmingCredentials);
         outState.putBoolean(SAVED_STATE_ENROLL_ACTION_LOGGED, mIsEnrollActionLogged);
+        if (mParentalOptions != null) {
+            outState.putBundle(SAVED_STATE_PARENTAL_OPTIONS, mParentalOptions);
+        }
         if (mGkPwHandle != null) {
             outState.putLong(SAVED_STATE_GK_PW_HANDLE, mGkPwHandle);
         }
@@ -204,31 +268,84 @@
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
 
+        // single enrollment is handled entirely by the launched activity
+        // this handles multi enroll or if parental consent is required
+        if (mParentalConsentHelper != null) {
+            handleOnActivityResultWhileConsenting(requestCode, resultCode, data);
+        } else {
+            handleOnActivityResultWhileEnrollingMultiple(requestCode, resultCode, data);
+        }
+    }
+
+    // handles responses while parental consent is pending
+    private void handleOnActivityResultWhileConsenting(
+            int requestCode, int resultCode, Intent data) {
+        overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
+
+        switch (requestCode) {
+            case REQUEST_CHOOSE_LOCK:
+            case REQUEST_CONFIRM_LOCK:
+                mConfirmingCredentials = false;
+                if (isSuccessfulConfirmOrChooseCredential(requestCode, resultCode)) {
+                    updateGatekeeperPasswordHandle(data);
+                    if (!mParentalConsentHelper.launchNext(this, REQUEST_CHOOSE_OPTIONS)) {
+                        Log.e(TAG, "Nothing to prompt for consent (no modalities enabled)!");
+                        finish();
+                    }
+                } else {
+                    Log.d(TAG, "Unknown result for set/choose lock: " + resultCode);
+                    setResult(resultCode);
+                    finish();
+                }
+                break;
+            case REQUEST_CHOOSE_OPTIONS:
+                if (resultCode == RESULT_CONSENT_GRANTED || resultCode == RESULT_CONSENT_DENIED) {
+                    final boolean isStillPrompting = mParentalConsentHelper.launchNext(
+                            this, REQUEST_CHOOSE_OPTIONS, resultCode, data);
+                    if (!isStillPrompting) {
+                        Log.d(TAG, "Enrollment options set, starting enrollment now");
+
+                        mParentalOptions = mParentalConsentHelper.getConsentResult();
+                        mParentalConsentHelper = null;
+                        startEnroll();
+                    }
+                } else {
+                    Log.d(TAG, "Unknown or cancelled parental consent");
+                    setResult(RESULT_CANCELED);
+                    finish();
+                }
+                break;
+            default:
+                Log.w(TAG, "Unknown consenting requestCode: " + requestCode + ", finishing");
+                finish();
+        }
+    }
+
+    // handles responses while multi biometric enrollment is pending
+    private void handleOnActivityResultWhileEnrollingMultiple(
+            int requestCode, int resultCode, Intent data) {
         if (mMultiBiometricEnrollHelper == null) {
             overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
 
             switch (requestCode) {
                 case REQUEST_CHOOSE_LOCK:
+                case REQUEST_CONFIRM_LOCK:
                     mConfirmingCredentials = false;
-                    if (resultCode == ChooseLockPattern.RESULT_FINISHED) {
-                        startMultiBiometricEnroll(data);
+                    final boolean isOk =
+                            isSuccessfulConfirmOrChooseCredential(requestCode, resultCode);
+                    // single modality enrollment requests confirmation directly
+                    // via BiometricEnrollBase#onCreate and should never get here
+                    if (isOk && mHasFeatureFace && mHasFeatureFingerprint) {
+                        updateGatekeeperPasswordHandle(data);
+                        launchFaceAndFingerprintEnroll();
                     } else {
-                        Log.d(TAG, "Unknown result for chooseLock: " + resultCode);
+                        Log.d(TAG, "Unknown result for set/choose lock: " + resultCode);
                         setResult(resultCode);
                         finish();
                     }
                     break;
-                case REQUEST_CONFIRM_LOCK:
-                    mConfirmingCredentials = false;
-                    if (resultCode == RESULT_OK) {
-                        startMultiBiometricEnroll(data);
-                    } else {
-                        Log.d(TAG, "Unknown result for confirmLock: " + resultCode);
-                        finish();
-                    }
-                    break;
                 default:
-                    Log.d(TAG, "Unknown requestCode: " + requestCode + ", finishing");
+                    Log.w(TAG, "Unknown enrolling requestCode: " + requestCode + ", finishing");
                     finish();
             }
         } else {
@@ -236,18 +353,28 @@
         }
     }
 
+    private static boolean isSuccessfulConfirmOrChooseCredential(int requestCode, int resultCode) {
+        final boolean okChoose = requestCode == REQUEST_CHOOSE_LOCK
+                && resultCode == ChooseLockPattern.RESULT_FINISHED;
+        final boolean okConfirm = requestCode == REQUEST_CONFIRM_LOCK
+                && resultCode == RESULT_OK;
+        return okChoose || okConfirm;
+    }
+
     @Override
     protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {
-        final int new_resid = SetupWizardUtils.getTheme(this, getIntent());
+        final int newResid = SetupWizardUtils.getTheme(this, getIntent());
         theme.applyStyle(R.style.SetupWizardPartnerResource, true);
-        super.onApplyThemeResource(theme, new_resid, first);
+        super.onApplyThemeResource(theme, newResid, first);
     }
 
     @Override
     protected void onStop() {
         super.onStop();
 
-        if (mConfirmingCredentials || mMultiBiometricEnrollHelper != null) {
+        if (mConfirmingCredentials
+                || mMultiBiometricEnrollHelper != null
+                || mParentalConsentHelper != null) {
             return;
         }
 
@@ -257,7 +384,8 @@
         }
     }
 
-    private void setupForMultiBiometricEnroll() {
+
+    private void setOrConfirmCredentialsNow() {
         if (!mConfirmingCredentials) {
             mConfirmingCredentials = true;
             if (!userHasPassword(mUserId)) {
@@ -268,37 +396,11 @@
         }
     }
 
-    private void startMultiBiometricEnroll(Intent data) {
-        final boolean isSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent());
-        final FingerprintManager fingerprintManager = getSystemService(FingerprintManager.class);
-        final FaceManager faceManager = getSystemService(FaceManager.class);
-        final List<FingerprintSensorPropertiesInternal> fpProperties =
-                fingerprintManager.getSensorPropertiesInternal();
-        final List<FaceSensorPropertiesInternal> faceProperties =
-                faceManager.getSensorPropertiesInternal();
-
+    private void updateGatekeeperPasswordHandle(@NonNull Intent data) {
         mGkPwHandle = BiometricUtils.getGatekeeperPasswordHandle(data);
-
-        if (isSetupWizard) {
-            // This would need to be updated for devices with multiple sensors of the same modality
-            mIsFaceEnrollable = !faceProperties.isEmpty()
-                    && faceManager.getEnrolledFaces(mUserId).size() == 0;
-            mIsFingerprintEnrollable = !fpProperties.isEmpty()
-                    && fingerprintManager.getEnrolledFingerprints(mUserId).size() == 0;
-        } else {
-            // This would need to be updated for devices with multiple sensors of the same modality
-            mIsFaceEnrollable = !faceProperties.isEmpty()
-                    && faceManager.getEnrolledFaces(mUserId).size()
-                    < faceProperties.get(0).maxEnrollmentsPerUser;
-            mIsFingerprintEnrollable = !fpProperties.isEmpty()
-                    && fingerprintManager.getEnrolledFingerprints(mUserId).size()
-                    < fpProperties.get(0).maxEnrollmentsPerUser;
-
+        if (mParentalConsentHelper != null) {
+            mParentalConsentHelper.updateGatekeeperHandle(data);
         }
-
-        mMultiBiometricEnrollHelper = new MultiBiometricEnrollHelper(this, mUserId,
-                mIsFaceEnrollable, mIsFingerprintEnrollable, mGkPwHandle);
-        mMultiBiometricEnrollHelper.startNextStep();
     }
 
     private boolean userHasPassword(int userId) {
@@ -310,6 +412,7 @@
 
     private void launchChooseLock() {
         Log.d(TAG, "launchChooseLock");
+
         Intent intent = BiometricUtils.getChooseLockIntent(this, getIntent());
         intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true);
         intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true);
@@ -323,6 +426,7 @@
 
     private void launchConfirmLock() {
         Log.d(TAG, "launchConfirmLock");
+
         final ChooseLockSettingsHelper.Builder builder = new ChooseLockSettingsHelper.Builder(this);
         builder.setRequestCode(REQUEST_CONFIRM_LOCK)
                 .setRequestGatekeeperPasswordHandle(true)
@@ -346,7 +450,7 @@
      * @param intent Enrollment activity that should be started (e.g. FaceEnrollIntroduction.class,
      *               etc).
      */
-    private void launchEnrollActivity(@NonNull Intent intent) {
+    private void launchSingleSensorEnrollActivity(@NonNull Intent intent) {
         intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
         byte[] hardwareAuthToken = null;
         if (this instanceof InternalActivity) {
@@ -362,7 +466,7 @@
         // If only device credential was specified, ask the user to only set that up.
         intent = new Intent(this, ChooseLockGeneric.class);
         intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true);
-        launchEnrollActivity(intent);
+        launchSingleSensorEnrollActivity(intent);
     }
 
     private void launchFingerprintOnlyEnroll() {
@@ -374,12 +478,18 @@
         } else {
             intent = BiometricUtils.getFingerprintIntroIntent(this, getIntent());
         }
-        launchEnrollActivity(intent);
+        launchSingleSensorEnrollActivity(intent);
     }
 
     private void launchFaceOnlyEnroll() {
         final Intent intent = BiometricUtils.getFaceIntroIntent(this, getIntent());
-        launchEnrollActivity(intent);
+        launchSingleSensorEnrollActivity(intent);
+    }
+
+    private void launchFaceAndFingerprintEnroll() {
+        mMultiBiometricEnrollHelper = new MultiBiometricEnrollHelper(this, mUserId,
+                mIsFaceEnrollable, mIsFingerprintEnrollable, mGkPwHandle);
+        mMultiBiometricEnrollHelper.startNextStep();
     }
 
     @Override
diff --git a/src/com/android/settings/biometrics/BiometricEnrollBase.java b/src/com/android/settings/biometrics/BiometricEnrollBase.java
index b62b35f..6e7d04f 100644
--- a/src/com/android/settings/biometrics/BiometricEnrollBase.java
+++ b/src/com/android/settings/biometrics/BiometricEnrollBase.java
@@ -26,6 +26,7 @@
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.text.TextUtils;
+import android.util.Log;
 import android.view.View;
 import android.widget.LinearLayout;
 import android.widget.TextView;
@@ -50,12 +51,15 @@
  */
 public abstract class BiometricEnrollBase extends InstrumentedActivity {
 
+    private static final String TAG = "BiometricEnrollBase";
+
     public static final String EXTRA_FROM_SETTINGS_SUMMARY = "from_settings_summary";
     public static final String EXTRA_KEY_LAUNCHED_CONFIRM = "launched_confirm_lock";
     public static final String EXTRA_KEY_REQUIRE_VISION = "accessibility_vision";
     public static final String EXTRA_KEY_REQUIRE_DIVERSITY = "accessibility_diversity";
     public static final String EXTRA_KEY_SENSOR_ID = "sensor_id";
     public static final String EXTRA_KEY_CHALLENGE = "challenge";
+    public static final String EXTRA_KEY_MODALITY = "sensor_modality";
 
     /**
      * Used by the choose fingerprint wizard to indicate the wizard is
@@ -84,11 +88,26 @@
      */
     public static final int RESULT_TIMEOUT = RESULT_FIRST_USER + 2;
 
+    /**
+     * Used by consent screens to indicate that consent was granted. Extras, such as
+     * EXTRA_KEY_MODALITY, will be included in the result to provide details about the
+     * consent that was granted.
+     */
+    public static final int RESULT_CONSENT_GRANTED = RESULT_FIRST_USER + 3;
+
+    /**
+     * Used by consent screens to indicate that consent was denied. Extras, such as
+     * EXTRA_KEY_MODALITY, will be included in the result to provide details about the
+     * consent that was not granted.
+     */
+    public static final int RESULT_CONSENT_DENIED = RESULT_FIRST_USER + 4;
+
     public static final int CHOOSE_LOCK_GENERIC_REQUEST = 1;
     public static final int BIOMETRIC_FIND_SENSOR_REQUEST = 2;
     public static final int LEARN_MORE_REQUEST = 3;
     public static final int CONFIRM_REQUEST = 4;
     public static final int ENROLL_REQUEST = 5;
+
     /**
      * Request code when starting another biometric enrollment from within a biometric flow. For
      * example, when starting fingerprint enroll after face enroll.
@@ -242,6 +261,8 @@
     }
 
     protected void launchConfirmLock(int titleResId) {
+        Log.d(TAG, "launchConfirmLock");
+
         final ChooseLockSettingsHelper.Builder builder = new ChooseLockSettingsHelper.Builder(this);
         builder.setRequestCode(CONFIRM_REQUEST)
                 .setTitle(getString(titleResId))
diff --git a/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java b/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java
index 580c753..c073c3c 100644
--- a/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java
@@ -294,15 +294,19 @@
             mConfirmingCredentials = false;
             if (resultCode == RESULT_FINISHED) {
                 updatePasswordQuality();
-                overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
-                getNextButton().setEnabled(false);
-                getChallenge(((sensorId, userId, challenge) -> {
-                    mSensorId = sensorId;
-                    mChallenge = challenge;
-                    mToken = BiometricUtils.requestGatekeeperHat(this, data, mUserId, challenge);
-                    BiometricUtils.removeGatekeeperPasswordHandle(this, data);
-                    getNextButton().setEnabled(true);
-                }));
+                final boolean handled = onSetOrConfirmCredentials(data);
+                if (!handled) {
+                    overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
+                    getNextButton().setEnabled(false);
+                    getChallenge(((sensorId, userId, challenge) -> {
+                        mSensorId = sensorId;
+                        mChallenge = challenge;
+                        mToken = BiometricUtils.requestGatekeeperHat(this, data, mUserId,
+                                challenge);
+                        BiometricUtils.removeGatekeeperPasswordHandle(this, data);
+                        getNextButton().setEnabled(true);
+                    }));
+                }
             } else {
                 setResult(resultCode, data);
                 finish();
@@ -310,15 +314,19 @@
         } else if (requestCode == CONFIRM_REQUEST) {
             mConfirmingCredentials = false;
             if (resultCode == RESULT_OK && data != null) {
-                overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
-                getNextButton().setEnabled(false);
-                getChallenge(((sensorId, userId, challenge) -> {
-                    mSensorId = sensorId;
-                    mChallenge = challenge;
-                    mToken = BiometricUtils.requestGatekeeperHat(this, data, mUserId, challenge);
-                    BiometricUtils.removeGatekeeperPasswordHandle(this, data);
-                    getNextButton().setEnabled(true);
-                }));
+                final boolean handled = onSetOrConfirmCredentials(data);
+                if (!handled) {
+                    overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
+                    getNextButton().setEnabled(false);
+                    getChallenge(((sensorId, userId, challenge) -> {
+                        mSensorId = sensorId;
+                        mChallenge = challenge;
+                        mToken = BiometricUtils.requestGatekeeperHat(this, data, mUserId,
+                                challenge);
+                        BiometricUtils.removeGatekeeperPasswordHandle(this, data);
+                        getNextButton().setEnabled(true);
+                    }));
+                }
             } else {
                 setResult(resultCode, data);
                 finish();
@@ -335,6 +343,18 @@
         super.onActivityResult(requestCode, resultCode, data);
     }
 
+    /**
+     * Called after confirming credentials. Can be used to prevent the default
+     * behavior of immediately calling #getChallenge (useful to things like intro
+     * consent screens that don't actually do enrollment and will later start an
+     * activity that does).
+     *
+     * @return True if the default behavior should be skipped and handled by this method instead.
+     */
+    protected boolean onSetOrConfirmCredentials(@Nullable Intent data) {
+        return false;
+    }
+
     protected void onCancelButtonClick(View view) {
         finish();
     }
diff --git a/src/com/android/settings/biometrics/ParentalConsentHelper.java b/src/com/android/settings/biometrics/ParentalConsentHelper.java
new file mode 100644
index 0000000..905a955
--- /dev/null
+++ b/src/com/android/settings/biometrics/ParentalConsentHelper.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2021 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 android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_MODALITY;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_CONSENT_DENIED;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_CONSENT_GRANTED;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.biometrics.face.FaceEnrollParentalConsent;
+import com.android.settings.biometrics.fingerprint.FingerprintEnrollParentalConsent;
+import com.android.settings.password.ChooseLockSettingsHelper;
+
+import com.google.android.setupcompat.util.WizardManagerHelper;
+
+/**
+ * Helper for {@link BiometricEnrollActivity} to ask for parental consent prior to actual user
+ * enrollment.
+ */
+public class ParentalConsentHelper {
+
+    private static final String KEY_FACE_CONSENT = "face";
+    private static final String KEY_FINGERPRINT_CONSENT = "fingerprint";
+
+    private final boolean mRequireFace;
+    private final boolean mRequireFingerprint;
+
+    private long mGkPwHandle;
+    @Nullable
+    private Boolean mConsentFace;
+    @Nullable
+    private Boolean mConsentFingerprint;
+
+    /**
+     * Helper for aggregating user consent.
+     *
+     * @param requireFace if face consent should be shown
+     * @param requireFingerprint if fingerprint consent should be shown
+     * @param gkPwHandle for launched intents
+     */
+    public ParentalConsentHelper(boolean requireFace, boolean requireFingerprint,
+            @Nullable Long gkPwHandle) {
+        mRequireFace = requireFace;
+        mRequireFingerprint = requireFingerprint;
+        mGkPwHandle = gkPwHandle != null ? gkPwHandle : 0L;
+    }
+
+    /**
+     * Updated the handle used for launching activities
+     *
+     * @param data result intent for credential verification
+     */
+    public void updateGatekeeperHandle(Intent data) {
+        mGkPwHandle = BiometricUtils.getGatekeeperPasswordHandle(data);
+    }
+
+    /**
+     * Launch the next consent screen.
+     *
+     * @param activity root activity
+     * @param requestCode request code to launch new activity
+     * @param resultCode result code of the last consent launch
+     * @param data result data from the last consent launch
+     * @return true if a consent activity was launched or false when complete
+     */
+    public boolean launchNext(@NonNull Activity activity, int requestCode, int resultCode,
+            @Nullable Intent data) {
+        if (data != null) {
+            switch (data.getIntExtra(EXTRA_KEY_MODALITY, TYPE_NONE)) {
+                case TYPE_FACE:
+                    mConsentFace = isConsent(resultCode, mConsentFace);
+                    break;
+                case TYPE_FINGERPRINT:
+                    mConsentFingerprint = isConsent(resultCode, mConsentFingerprint);
+                    break;
+            }
+        }
+        return launchNext(activity, requestCode);
+    }
+
+    @Nullable
+    private static Boolean isConsent(int resultCode, @Nullable Boolean defaultValue) {
+        switch (resultCode) {
+            case RESULT_CONSENT_GRANTED:
+                return true;
+            case RESULT_CONSENT_DENIED:
+                return false;
+        }
+        return defaultValue;
+    }
+
+    /** @see #launchNext(Activity, int, int, Intent)  */
+    public boolean launchNext(@NonNull Activity activity, int requestCode) {
+        final Intent intent = getNextConsentIntent(activity);
+        if (intent != null) {
+            WizardManagerHelper.copyWizardManagerExtras(activity.getIntent(), intent);
+            if (mGkPwHandle != 0) {
+                intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, mGkPwHandle);
+            }
+            activity.startActivityForResult(intent, requestCode);
+            return true;
+        }
+        return false;
+    }
+
+    @Nullable
+    private Intent getNextConsentIntent(@NonNull Context context) {
+        if (mRequireFace && mConsentFace == null) {
+            return new Intent(context, FaceEnrollParentalConsent.class);
+        }
+        if (mRequireFingerprint && mConsentFingerprint == null) {
+            return new Intent(context, FingerprintEnrollParentalConsent.class);
+        }
+        return null;
+    }
+
+    /**
+     * Get the result of all consent requests.
+     *
+     * This should be called when {@link #launchNext(Activity, int, int, Intent)} returns false
+     * to indicate that all responses have been recorded.
+     *
+     * @return The aggregate consent status.
+     */
+    @NonNull
+    public Bundle getConsentResult() {
+        final Bundle result = new Bundle();
+        result.putBoolean(KEY_FACE_CONSENT, mConsentFace != null ? mConsentFace : false);
+        result.putBoolean(KEY_FINGERPRINT_CONSENT,
+                mConsentFingerprint != null ? mConsentFingerprint : false);
+        return result;
+    }
+
+    /** @return If the result bundle contains consent for face authentication. */
+    public static boolean hasFaceConsent(@NonNull Bundle bundle) {
+        return bundle.getBoolean(KEY_FACE_CONSENT, false);
+    }
+
+    /** @return If the result bundle contains consent for fingerprint authentication. */
+    public static boolean hasFingerprintConsent(@NonNull Bundle bundle) {
+        return bundle.getBoolean(KEY_FINGERPRINT_CONSENT, false);
+    }
+}
diff --git a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
index 8e733b4..fc13a8c 100644
--- a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
@@ -85,22 +85,28 @@
         mFaceFeatureProvider = FeatureFactory.getFactory(getApplicationContext())
                 .getFaceFeatureProvider();
 
-
         // This path is an entry point for SetNewPasswordController, e.g.
         // adb shell am start -a android.app.action.SET_NEW_PASSWORD
         if (mToken == null && BiometricUtils.containsGatekeeperPasswordHandle(getIntent())) {
-            mFooterBarMixin.getPrimaryButton().setEnabled(false);
-            // We either block on generateChallenge, or need to gray out the "next" button until
-            // the challenge is ready. Let's just do this for now.
-            mFaceManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> {
-                mToken = BiometricUtils.requestGatekeeperHat(this, getIntent(), mUserId, challenge);
-                mSensorId = sensorId;
-                mChallenge = challenge;
-                mFooterBarMixin.getPrimaryButton().setEnabled(true);
-            });
+            if (generateChallengeOnCreate()) {
+                mFooterBarMixin.getPrimaryButton().setEnabled(false);
+                // We either block on generateChallenge, or need to gray out the "next" button until
+                // the challenge is ready. Let's just do this for now.
+                mFaceManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> {
+                    mToken = BiometricUtils.requestGatekeeperHat(this, getIntent(), mUserId,
+                            challenge);
+                    mSensorId = sensorId;
+                    mChallenge = challenge;
+                    mFooterBarMixin.getPrimaryButton().setEnabled(true);
+                });
+            }
         }
     }
 
+    protected boolean generateChallengeOnCreate() {
+        return true;
+    }
+
     @Override
     protected boolean isDisabledByAdmin() {
         return RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled(
diff --git a/src/com/android/settings/biometrics/face/FaceEnrollParentalConsent.java b/src/com/android/settings/biometrics/face/FaceEnrollParentalConsent.java
new file mode 100644
index 0000000..8e80b39
--- /dev/null
+++ b/src/com/android/settings/biometrics/face/FaceEnrollParentalConsent.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2021 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 static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
+
+import android.content.Intent;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+
+/**
+ * Displays parental consent information for face authentication.
+ *
+ * TODO(b/188847063): swap strings for consent screen
+ */
+public class FaceEnrollParentalConsent extends FaceEnrollIntroduction {
+
+    @Override
+    protected void onNextButtonClick(View view) {
+        onConsentResult(true /* granted */);
+    }
+
+    @Override
+    protected void onSkipButtonClick(View view) {
+        onConsentResult(false /* granted */);
+    }
+
+    private void onConsentResult(boolean granted) {
+        final Intent result = new Intent();
+        result.putExtra(EXTRA_KEY_MODALITY, TYPE_FACE);
+        setResult(granted ? RESULT_CONSENT_GRANTED : RESULT_CONSENT_DENIED, result);
+        finish();
+    }
+
+    @Override
+    protected boolean onSetOrConfirmCredentials(@Nullable Intent data) {
+        // prevent challenge from being generated by default
+        return true;
+    }
+
+    @Override
+    protected boolean generateChallengeOnCreate() {
+        return false;
+    }
+
+    @Override
+    protected int getHeaderResDefault() {
+        return R.string.security_settings_face_enroll_consent_introduction_title;
+    }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollParentalConsent.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollParentalConsent.java
new file mode 100644
index 0000000..8920307
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollParentalConsent.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 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.fingerprint;
+
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+
+import android.content.Intent;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+
+/**
+ * Displays parental consent information for fingerprint authentication.
+ *
+ * TODO(b/188847063): swap strings for consent screen
+ */
+public class FingerprintEnrollParentalConsent extends FingerprintEnrollIntroduction {
+
+    @Override
+    protected void onNextButtonClick(View view) {
+        onConsentResult(true /* granted */);
+    }
+
+    @Override
+    protected void onSkipButtonClick(View view) {
+        onConsentResult(false /* granted */);
+    }
+
+    private void onConsentResult(boolean granted) {
+        final Intent result = new Intent();
+        result.putExtra(EXTRA_KEY_MODALITY, TYPE_FINGERPRINT);
+        setResult(granted ? RESULT_CONSENT_GRANTED : RESULT_CONSENT_DENIED, result);
+        finish();
+    }
+
+    @Override
+    protected boolean onSetOrConfirmCredentials(@Nullable Intent data) {
+        // prevent challenge from being generated by default
+        return true;
+    }
+
+    @Override
+    protected int getHeaderResDefault() {
+        return R.string.security_settings_fingerprint_enroll_consent_introduction_title;
+    }
+}
diff --git a/tests/unit/src/com/android/settings/biometrics/ParentalConsentHelperTest.java b/tests/unit/src/com/android/settings/biometrics/ParentalConsentHelperTest.java
new file mode 100644
index 0000000..78856da
--- /dev/null
+++ b/tests/unit/src/com/android/settings/biometrics/ParentalConsentHelperTest.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2021 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 android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE;
+
+import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_MODALITY;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_CONSENT_DENIED;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_CONSENT_GRANTED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.hardware.biometrics.BiometricAuthenticator;
+import android.util.Pair;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settings.biometrics.face.FaceEnrollParentalConsent;
+import com.android.settings.biometrics.fingerprint.FingerprintEnrollParentalConsent;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RunWith(AndroidJUnit4.class)
+public class ParentalConsentHelperTest {
+
+    private static final int REQUEST_CODE = 12;
+
+    @Rule
+    public final MockitoRule mMocks = MockitoJUnit.rule();
+
+    @Mock
+    private Activity mRootActivity;
+    @Mock
+    private Intent mRootActivityIntent;
+    @Captor
+    ArgumentCaptor<Intent> mLastStarted;
+
+    @Before
+    public void setup() {
+        when(mRootActivity.getIntent()).thenAnswer(invocation -> mRootActivityIntent);
+        when(mRootActivityIntent.getBundleExtra(any())).thenAnswer(invocation -> null);
+        when(mRootActivityIntent.getStringExtra(any())).thenAnswer(invocation -> null);
+        when(mRootActivityIntent.getBooleanExtra(any(), anyBoolean()))
+                .thenAnswer(invocation -> invocation.getArguments()[1]);
+    }
+
+    @Test
+    public void testLaunchNext_face_and_fingerprint_all_consent() {
+        testLaunchNext(
+                true /* requireFace */, true /* grantFace */,
+                true /* requireFingerprint */, true /* grantFace */,
+                90 /* gkpw */);
+    }
+
+    @Test
+    public void testLaunchNext_nothing_to_consent() {
+        testLaunchNext(
+                false /* requireFace */, false /* grantFace */,
+                false /* requireFingerprint */, false /* grantFace */,
+                80 /* gkpw */);
+    }
+
+    @Test
+    public void testLaunchNext_face_and_fingerprint_no_consent() {
+        testLaunchNext(
+                true /* requireFace */, false /* grantFace */,
+                true /* requireFingerprint */, false /* grantFace */,
+                70 /* gkpw */);
+    }
+
+    @Test
+    public void testLaunchNext_face_and_fingerprint_only_face_consent() {
+        testLaunchNext(
+                true /* requireFace */, true /* grantFace */,
+                true /* requireFingerprint */, false /* grantFace */,
+                60 /* gkpw */);
+    }
+
+    @Test
+    public void testLaunchNext_face_and_fingerprint_only_fingerprint_consent() {
+        testLaunchNext(
+                true /* requireFace */, false /* grantFace */,
+                true /* requireFingerprint */, true /* grantFace */,
+                50 /* gkpw */);
+    }
+
+    @Test
+    public void testLaunchNext_face_with_consent() {
+        testLaunchNext(
+                true /* requireFace */, true /* grantFace */,
+                false /* requireFingerprint */, false /* grantFace */,
+                40 /* gkpw */);
+    }
+
+    @Test
+    public void testLaunchNext_face_without_consent() {
+        testLaunchNext(
+                true /* requireFace */, false /* grantFace */,
+                false /* requireFingerprint */, false /* grantFace */,
+                30 /* gkpw */);
+    }
+
+    @Test
+    public void testLaunchNext_fingerprint_with_consent() {
+        testLaunchNext(
+                false /* requireFace */, false /* grantFace */,
+                true /* requireFingerprint */, true /* grantFace */,
+                20 /* gkpw */);
+    }
+
+    @Test
+    public void testLaunchNext_fingerprint_without_consent() {
+        testLaunchNext(
+                false /* requireFace */, false /* grantFace */,
+                true /* requireFingerprint */, false /* grantFace */,
+                10 /* gkpw */);
+    }
+
+    private void testLaunchNext(
+            boolean requireFace, boolean grantFace,
+            boolean requireFingerprint, boolean grantFingerprint,
+            long gkpw) {
+        final List<Pair<String, Boolean>> expectedLaunches = new ArrayList<>();
+        if (requireFace) {
+            expectedLaunches.add(new Pair(FaceEnrollParentalConsent.class.getName(), grantFace));
+        }
+        if (requireFingerprint) {
+            expectedLaunches.add(
+                    new Pair(FingerprintEnrollParentalConsent.class.getName(), grantFingerprint));
+        }
+
+        // initial consent status
+        final ParentalConsentHelper helper =
+                new ParentalConsentHelper(requireFace, requireFingerprint, gkpw);
+        assertThat(ParentalConsentHelper.hasFaceConsent(helper.getConsentResult()))
+                .isFalse();
+        assertThat(ParentalConsentHelper.hasFingerprintConsent(helper.getConsentResult()))
+                .isFalse();
+
+        // check expected launches
+        for (int i = 0; i <= expectedLaunches.size(); i++) {
+            final Pair<String, Boolean> expected = i > 0 ? expectedLaunches.get(i - 1) : null;
+            final boolean launchedNext = i == 0
+                    ? helper.launchNext(mRootActivity, REQUEST_CODE)
+                    : helper.launchNext(mRootActivity, REQUEST_CODE,
+                            expected.second ? RESULT_CONSENT_GRANTED : RESULT_CONSENT_DENIED,
+                            getResultIntent(getStartedModality(expected.first)));
+            assertThat(launchedNext).isEqualTo(i < expectedLaunches.size());
+        }
+        verify(mRootActivity, times(expectedLaunches.size()))
+                .startActivityForResult(mLastStarted.capture(), eq(REQUEST_CODE));
+        assertThat(mLastStarted.getAllValues()
+                .stream().map(i -> i.getComponent().getClassName()).collect(Collectors.toList()))
+                .containsExactlyElementsIn(
+                        expectedLaunches.stream().map(i -> i.first).collect(Collectors.toList()))
+                .inOrder();
+        if (!expectedLaunches.isEmpty()) {
+            assertThat(mLastStarted.getAllValues()
+                    .stream().map(BiometricUtils::getGatekeeperPasswordHandle).distinct()
+                    .collect(Collectors.toList()))
+                    .containsExactly(gkpw);
+        }
+
+        // final consent status
+        assertThat(ParentalConsentHelper.hasFaceConsent(helper.getConsentResult()))
+                .isEqualTo(requireFace && grantFace);
+        assertThat(ParentalConsentHelper.hasFingerprintConsent(helper.getConsentResult()))
+                .isEqualTo(requireFingerprint && grantFingerprint);
+    }
+
+    private static Intent getResultIntent(@BiometricAuthenticator.Modality int modality) {
+        final Intent intent = new Intent();
+        intent.putExtra(EXTRA_KEY_MODALITY, modality);
+        return intent;
+    }
+
+    @BiometricAuthenticator.Modality
+    private static int getStartedModality(String name) {
+        if (name.equals(FaceEnrollParentalConsent.class.getName())) {
+            return TYPE_FACE;
+        }
+        if (name.equals(FingerprintEnrollParentalConsent.class.getName())) {
+            return TYPE_FINGERPRINT;
+        }
+        return TYPE_NONE;
+    }
+}