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;
+ }
+}