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
This commit is contained in:
Joe Bolinger
2021-06-10 13:36:38 -07:00
parent d0adfa7b3e
commit a8808f7368
10 changed files with 801 additions and 114 deletions

View File

@@ -1803,6 +1803,10 @@
android:theme="@style/GlifV3Theme.Light" android:theme="@style/GlifV3Theme.Light"
android:exported="false"/> android:exported="false"/>
<activity android:name=".biometrics.face.FaceEnrollParentalConsent"
android:exported="false"
android:screenOrientation="portrait"/>
<activity android:name=".biometrics.face.FaceEnrollIntroduction" <activity android:name=".biometrics.face.FaceEnrollIntroduction"
android:exported="false" android:exported="false"
android:screenOrientation="portrait"/> android:screenOrientation="portrait"/>
@@ -1837,6 +1841,7 @@
<activity android:name=".biometrics.fingerprint.FingerprintEnrollFindSensor" android:exported="false"/> <activity android:name=".biometrics.fingerprint.FingerprintEnrollFindSensor" android:exported="false"/>
<activity android:name=".biometrics.fingerprint.FingerprintEnrollEnrolling" 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.FingerprintEnrollFinish" android:exported="false"/>
<activity android:name=".biometrics.fingerprint.FingerprintEnrollParentalConsent" android:exported="false"/>
<activity android:name=".biometrics.fingerprint.FingerprintEnrollIntroduction" <activity android:name=".biometrics.fingerprint.FingerprintEnrollIntroduction"
android:exported="true" android:exported="true"
android:theme="@style/GlifTheme.Light"> android:theme="@style/GlifTheme.Light">

View File

@@ -777,6 +777,8 @@
<string name="security_settings_face_enroll_introduction_more">More</string> <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] --> <!-- 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> <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] --> <!-- 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> <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]--> <!-- Introduction detail message shown in face enrollment dialog [CHAR LIMIT=NONE]-->
@@ -888,8 +890,10 @@
</plurals> </plurals>
<!-- message shown in summary field when no fingerprints are registered --> <!-- message shown in summary field when no fingerprints are registered -->
<string name="security_settings_fingerprint_preference_summary_none"></string> <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> <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] --> <!-- 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> <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]--> <!-- Introduction detail message shown in fingerprint enrollment dialog [CHAR LIMIT=NONE]-->

View File

@@ -19,6 +19,9 @@ package com.android.settings.biometrics;
import static android.provider.Settings.ACTION_BIOMETRIC_ENROLL; import static android.provider.Settings.ACTION_BIOMETRIC_ENROLL;
import static android.provider.Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED; 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.annotation.NonNull;
import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManager;
import android.app.settings.SettingsEnums; import android.app.settings.SettingsEnums;
@@ -64,6 +67,8 @@ public class BiometricEnrollActivity extends InstrumentedActivity {
private static final int REQUEST_CHOOSE_LOCK = 1; private static final int REQUEST_CHOOSE_LOCK = 1;
private static final int REQUEST_CONFIRM_LOCK = 2; 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; public static final int RESULT_SKIP = BiometricEnrollBase.RESULT_SKIP;
@@ -71,8 +76,12 @@ public class BiometricEnrollActivity extends InstrumentedActivity {
// this only applies to fingerprint. // this only applies to fingerprint.
public static final String EXTRA_SKIP_INTRO = "skip_intro"; 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_CONFIRMING_CREDENTIALS = "confirming_credentials";
private static final String SAVED_STATE_ENROLL_ACTION_LOGGED = "enroll_action_logged"; 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"; private static final String SAVED_STATE_GK_PW_HANDLE = "gk_pw_handle";
public static final class InternalActivity extends BiometricEnrollActivity {} public static final class InternalActivity extends BiometricEnrollActivity {}
@@ -80,9 +89,14 @@ public class BiometricEnrollActivity extends InstrumentedActivity {
private int mUserId = UserHandle.myUserId(); private int mUserId = UserHandle.myUserId();
private boolean mConfirmingCredentials; private boolean mConfirmingCredentials;
private boolean mIsEnrollActionLogged; private boolean mIsEnrollActionLogged;
private boolean mIsFaceEnrollable; private boolean mHasFeatureFace = false;
private boolean mIsFingerprintEnrollable; 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 Long mGkPwHandle;
@Nullable private ParentalConsentHelper mParentalConsentHelper;
@Nullable private MultiBiometricEnrollHelper mMultiBiometricEnrollHelper; @Nullable private MultiBiometricEnrollHelper mMultiBiometricEnrollHelper;
@Override @Override
@@ -101,6 +115,7 @@ public class BiometricEnrollActivity extends InstrumentedActivity {
SAVED_STATE_CONFIRMING_CREDENTIALS, false); SAVED_STATE_CONFIRMING_CREDENTIALS, false);
mIsEnrollActionLogged = savedInstanceState.getBoolean( mIsEnrollActionLogged = savedInstanceState.getBoolean(
SAVED_STATE_ENROLL_ACTION_LOGGED, false); SAVED_STATE_ENROLL_ACTION_LOGGED, false);
mParentalOptions = savedInstanceState.getBundle(SAVED_STATE_PARENTAL_OPTIONS);
if (savedInstanceState.containsKey(SAVED_STATE_GK_PW_HANDLE)) { if (savedInstanceState.containsKey(SAVED_STATE_GK_PW_HANDLE)) {
mGkPwHandle = savedInstanceState.getLong(SAVED_STATE_GK_PW_HANDLE); mGkPwHandle = savedInstanceState.getLong(SAVED_STATE_GK_PW_HANDLE);
} }
@@ -141,52 +156,98 @@ public class BiometricEnrollActivity extends InstrumentedActivity {
SetupWizardUtils.getThemeString(intent)); SetupWizardUtils.getThemeString(intent));
} }
// Default behavior is to enroll BIOMETRIC_WEAK or above. See ACTION_BIOMETRIC_ENROLL. final PackageManager pm = getApplicationContext().getPackageManager();
final int authenticators = intent.getIntExtra( mHasFeatureFingerprint = pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT);
EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, Authenticators.BIOMETRIC_WEAK); 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); Log.d(TAG, "Authenticators: " + authenticators);
final PackageManager pm = getApplicationContext().getPackageManager(); startEnrollWith(authenticators, WizardManagerHelper.isAnySetupWizard(getIntent()));
final boolean hasFeatureFingerprint = }
pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT);
final boolean hasFeatureFace = pm.hasSystemFeature(PackageManager.FEATURE_FACE);
final boolean isSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent());
if (isSetupWizard) { private void startEnrollWith(@Authenticators.Types int authenticators, boolean setupWizard) {
if (hasFeatureFace && hasFeatureFingerprint) { // If the caller is not setup wizard, and the user has something enrolled, finish.
setupForMultiBiometricEnroll(); if (!setupWizard) {
} 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.
final BiometricManager bm = getSystemService(BiometricManager.class); final BiometricManager bm = getSystemService(BiometricManager.class);
final @BiometricError int result = bm.canAuthenticate(authenticators); final @BiometricError int result = bm.canAuthenticate(authenticators);
if (result != BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { if (result != BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
Log.e(TAG, "Unexpected result: " + result); Log.e(TAG, "Unexpected result (has enrollments): " + result);
finish(); finish();
return; return;
} }
}
// This will need to be updated if the device has sensors other than BIOMETRIC_STRONG // This will need to be updated if the device has sensors other than BIOMETRIC_STRONG
if (authenticators == BiometricManager.Authenticators.DEVICE_CREDENTIAL) { if (!setupWizard && authenticators == BiometricManager.Authenticators.DEVICE_CREDENTIAL) {
launchCredentialOnlyEnroll(); launchCredentialOnlyEnroll();
} else if (hasFeatureFace && hasFeatureFingerprint) { } else if (mHasFeatureFace && mHasFeatureFingerprint) {
setupForMultiBiometricEnroll(); if (mParentalOptionsRequired && mGkPwHandle != null) {
} else if (hasFeatureFingerprint) { launchFaceAndFingerprintEnroll();
launchFingerprintOnlyEnroll();
} else if (hasFeatureFace) {
launchFaceOnlyEnroll();
} else { } else {
Log.e(TAG, "Unknown state, finishing"); setOrConfirmCredentialsNow();
finish();
} }
} else if (mHasFeatureFingerprint) {
launchFingerprintOnlyEnroll();
} else if (mHasFeatureFace) {
launchFaceOnlyEnroll();
} else {
Log.e(TAG, "Unknown state, finishing (was SUW: " + setupWizard + ")");
finish();
} }
} }
@@ -195,6 +256,9 @@ public class BiometricEnrollActivity extends InstrumentedActivity {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putBoolean(SAVED_STATE_CONFIRMING_CREDENTIALS, mConfirmingCredentials); outState.putBoolean(SAVED_STATE_CONFIRMING_CREDENTIALS, mConfirmingCredentials);
outState.putBoolean(SAVED_STATE_ENROLL_ACTION_LOGGED, mIsEnrollActionLogged); outState.putBoolean(SAVED_STATE_ENROLL_ACTION_LOGGED, mIsEnrollActionLogged);
if (mParentalOptions != null) {
outState.putBundle(SAVED_STATE_PARENTAL_OPTIONS, mParentalOptions);
}
if (mGkPwHandle != null) { if (mGkPwHandle != null) {
outState.putLong(SAVED_STATE_GK_PW_HANDLE, mGkPwHandle); outState.putLong(SAVED_STATE_GK_PW_HANDLE, mGkPwHandle);
} }
@@ -204,31 +268,84 @@ public class BiometricEnrollActivity extends InstrumentedActivity {
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, 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) { if (mMultiBiometricEnrollHelper == null) {
overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out); overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
switch (requestCode) { switch (requestCode) {
case REQUEST_CHOOSE_LOCK: case REQUEST_CHOOSE_LOCK:
case REQUEST_CONFIRM_LOCK:
mConfirmingCredentials = false; mConfirmingCredentials = false;
if (resultCode == ChooseLockPattern.RESULT_FINISHED) { final boolean isOk =
startMultiBiometricEnroll(data); 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 { } else {
Log.d(TAG, "Unknown result for chooseLock: " + resultCode); Log.d(TAG, "Unknown result for set/choose lock: " + resultCode);
setResult(resultCode); setResult(resultCode);
finish(); finish();
} }
break; 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: default:
Log.d(TAG, "Unknown requestCode: " + requestCode + ", finishing"); Log.w(TAG, "Unknown enrolling requestCode: " + requestCode + ", finishing");
finish(); finish();
} }
} else { } else {
@@ -236,18 +353,28 @@ public class BiometricEnrollActivity extends InstrumentedActivity {
} }
} }
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 @Override
protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) { 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); theme.applyStyle(R.style.SetupWizardPartnerResource, true);
super.onApplyThemeResource(theme, new_resid, first); super.onApplyThemeResource(theme, newResid, first);
} }
@Override @Override
protected void onStop() { protected void onStop() {
super.onStop(); super.onStop();
if (mConfirmingCredentials || mMultiBiometricEnrollHelper != null) { if (mConfirmingCredentials
|| mMultiBiometricEnrollHelper != null
|| mParentalConsentHelper != null) {
return; return;
} }
@@ -257,7 +384,8 @@ public class BiometricEnrollActivity extends InstrumentedActivity {
} }
} }
private void setupForMultiBiometricEnroll() {
private void setOrConfirmCredentialsNow() {
if (!mConfirmingCredentials) { if (!mConfirmingCredentials) {
mConfirmingCredentials = true; mConfirmingCredentials = true;
if (!userHasPassword(mUserId)) { if (!userHasPassword(mUserId)) {
@@ -268,37 +396,11 @@ public class BiometricEnrollActivity extends InstrumentedActivity {
} }
} }
private void startMultiBiometricEnroll(Intent data) { private void updateGatekeeperPasswordHandle(@NonNull 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();
mGkPwHandle = BiometricUtils.getGatekeeperPasswordHandle(data); mGkPwHandle = BiometricUtils.getGatekeeperPasswordHandle(data);
if (mParentalConsentHelper != null) {
if (isSetupWizard) { mParentalConsentHelper.updateGatekeeperHandle(data);
// 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;
} }
mMultiBiometricEnrollHelper = new MultiBiometricEnrollHelper(this, mUserId,
mIsFaceEnrollable, mIsFingerprintEnrollable, mGkPwHandle);
mMultiBiometricEnrollHelper.startNextStep();
} }
private boolean userHasPassword(int userId) { private boolean userHasPassword(int userId) {
@@ -310,6 +412,7 @@ public class BiometricEnrollActivity extends InstrumentedActivity {
private void launchChooseLock() { private void launchChooseLock() {
Log.d(TAG, "launchChooseLock"); Log.d(TAG, "launchChooseLock");
Intent intent = BiometricUtils.getChooseLockIntent(this, getIntent()); Intent intent = BiometricUtils.getChooseLockIntent(this, getIntent());
intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true); intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true);
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true);
@@ -323,6 +426,7 @@ public class BiometricEnrollActivity extends InstrumentedActivity {
private void launchConfirmLock() { private void launchConfirmLock() {
Log.d(TAG, "launchConfirmLock"); Log.d(TAG, "launchConfirmLock");
final ChooseLockSettingsHelper.Builder builder = new ChooseLockSettingsHelper.Builder(this); final ChooseLockSettingsHelper.Builder builder = new ChooseLockSettingsHelper.Builder(this);
builder.setRequestCode(REQUEST_CONFIRM_LOCK) builder.setRequestCode(REQUEST_CONFIRM_LOCK)
.setRequestGatekeeperPasswordHandle(true) .setRequestGatekeeperPasswordHandle(true)
@@ -346,7 +450,7 @@ public class BiometricEnrollActivity extends InstrumentedActivity {
* @param intent Enrollment activity that should be started (e.g. FaceEnrollIntroduction.class, * @param intent Enrollment activity that should be started (e.g. FaceEnrollIntroduction.class,
* etc). * etc).
*/ */
private void launchEnrollActivity(@NonNull Intent intent) { private void launchSingleSensorEnrollActivity(@NonNull Intent intent) {
intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
byte[] hardwareAuthToken = null; byte[] hardwareAuthToken = null;
if (this instanceof InternalActivity) { if (this instanceof InternalActivity) {
@@ -362,7 +466,7 @@ public class BiometricEnrollActivity extends InstrumentedActivity {
// If only device credential was specified, ask the user to only set that up. // If only device credential was specified, ask the user to only set that up.
intent = new Intent(this, ChooseLockGeneric.class); intent = new Intent(this, ChooseLockGeneric.class);
intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true); intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true);
launchEnrollActivity(intent); launchSingleSensorEnrollActivity(intent);
} }
private void launchFingerprintOnlyEnroll() { private void launchFingerprintOnlyEnroll() {
@@ -374,12 +478,18 @@ public class BiometricEnrollActivity extends InstrumentedActivity {
} else { } else {
intent = BiometricUtils.getFingerprintIntroIntent(this, getIntent()); intent = BiometricUtils.getFingerprintIntroIntent(this, getIntent());
} }
launchEnrollActivity(intent); launchSingleSensorEnrollActivity(intent);
} }
private void launchFaceOnlyEnroll() { private void launchFaceOnlyEnroll() {
final Intent intent = BiometricUtils.getFaceIntroIntent(this, getIntent()); 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 @Override

View File

@@ -26,6 +26,7 @@ import android.graphics.Color;
import android.os.Bundle; import android.os.Bundle;
import android.os.UserHandle; import android.os.UserHandle;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
@@ -50,12 +51,15 @@ import com.google.android.setupdesign.util.ThemeHelper;
*/ */
public abstract class BiometricEnrollBase extends InstrumentedActivity { 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_FROM_SETTINGS_SUMMARY = "from_settings_summary";
public static final String EXTRA_KEY_LAUNCHED_CONFIRM = "launched_confirm_lock"; 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_VISION = "accessibility_vision";
public static final String EXTRA_KEY_REQUIRE_DIVERSITY = "accessibility_diversity"; 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_SENSOR_ID = "sensor_id";
public static final String EXTRA_KEY_CHALLENGE = "challenge"; 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 * Used by the choose fingerprint wizard to indicate the wizard is
@@ -84,11 +88,26 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity {
*/ */
public static final int RESULT_TIMEOUT = RESULT_FIRST_USER + 2; 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 CHOOSE_LOCK_GENERIC_REQUEST = 1;
public static final int BIOMETRIC_FIND_SENSOR_REQUEST = 2; public static final int BIOMETRIC_FIND_SENSOR_REQUEST = 2;
public static final int LEARN_MORE_REQUEST = 3; public static final int LEARN_MORE_REQUEST = 3;
public static final int CONFIRM_REQUEST = 4; public static final int CONFIRM_REQUEST = 4;
public static final int ENROLL_REQUEST = 5; public static final int ENROLL_REQUEST = 5;
/** /**
* Request code when starting another biometric enrollment from within a biometric flow. For * Request code when starting another biometric enrollment from within a biometric flow. For
* example, when starting fingerprint enroll after face enroll. * example, when starting fingerprint enroll after face enroll.
@@ -242,6 +261,8 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity {
} }
protected void launchConfirmLock(int titleResId) { protected void launchConfirmLock(int titleResId) {
Log.d(TAG, "launchConfirmLock");
final ChooseLockSettingsHelper.Builder builder = new ChooseLockSettingsHelper.Builder(this); final ChooseLockSettingsHelper.Builder builder = new ChooseLockSettingsHelper.Builder(this);
builder.setRequestCode(CONFIRM_REQUEST) builder.setRequestCode(CONFIRM_REQUEST)
.setTitle(getString(titleResId)) .setTitle(getString(titleResId))

View File

@@ -294,15 +294,19 @@ public abstract class BiometricEnrollIntroduction extends BiometricEnrollBase
mConfirmingCredentials = false; mConfirmingCredentials = false;
if (resultCode == RESULT_FINISHED) { if (resultCode == RESULT_FINISHED) {
updatePasswordQuality(); updatePasswordQuality();
overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out); final boolean handled = onSetOrConfirmCredentials(data);
getNextButton().setEnabled(false); if (!handled) {
getChallenge(((sensorId, userId, challenge) -> { overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
mSensorId = sensorId; getNextButton().setEnabled(false);
mChallenge = challenge; getChallenge(((sensorId, userId, challenge) -> {
mToken = BiometricUtils.requestGatekeeperHat(this, data, mUserId, challenge); mSensorId = sensorId;
BiometricUtils.removeGatekeeperPasswordHandle(this, data); mChallenge = challenge;
getNextButton().setEnabled(true); mToken = BiometricUtils.requestGatekeeperHat(this, data, mUserId,
})); challenge);
BiometricUtils.removeGatekeeperPasswordHandle(this, data);
getNextButton().setEnabled(true);
}));
}
} else { } else {
setResult(resultCode, data); setResult(resultCode, data);
finish(); finish();
@@ -310,15 +314,19 @@ public abstract class BiometricEnrollIntroduction extends BiometricEnrollBase
} else if (requestCode == CONFIRM_REQUEST) { } else if (requestCode == CONFIRM_REQUEST) {
mConfirmingCredentials = false; mConfirmingCredentials = false;
if (resultCode == RESULT_OK && data != null) { if (resultCode == RESULT_OK && data != null) {
overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out); final boolean handled = onSetOrConfirmCredentials(data);
getNextButton().setEnabled(false); if (!handled) {
getChallenge(((sensorId, userId, challenge) -> { overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
mSensorId = sensorId; getNextButton().setEnabled(false);
mChallenge = challenge; getChallenge(((sensorId, userId, challenge) -> {
mToken = BiometricUtils.requestGatekeeperHat(this, data, mUserId, challenge); mSensorId = sensorId;
BiometricUtils.removeGatekeeperPasswordHandle(this, data); mChallenge = challenge;
getNextButton().setEnabled(true); mToken = BiometricUtils.requestGatekeeperHat(this, data, mUserId,
})); challenge);
BiometricUtils.removeGatekeeperPasswordHandle(this, data);
getNextButton().setEnabled(true);
}));
}
} else { } else {
setResult(resultCode, data); setResult(resultCode, data);
finish(); finish();
@@ -335,6 +343,18 @@ public abstract class BiometricEnrollIntroduction extends BiometricEnrollBase
super.onActivityResult(requestCode, resultCode, data); 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) { protected void onCancelButtonClick(View view) {
finish(); finish();
} }

View File

@@ -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);
}
}

View File

@@ -85,22 +85,28 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
mFaceFeatureProvider = FeatureFactory.getFactory(getApplicationContext()) mFaceFeatureProvider = FeatureFactory.getFactory(getApplicationContext())
.getFaceFeatureProvider(); .getFaceFeatureProvider();
// This path is an entry point for SetNewPasswordController, e.g. // This path is an entry point for SetNewPasswordController, e.g.
// adb shell am start -a android.app.action.SET_NEW_PASSWORD // adb shell am start -a android.app.action.SET_NEW_PASSWORD
if (mToken == null && BiometricUtils.containsGatekeeperPasswordHandle(getIntent())) { if (mToken == null && BiometricUtils.containsGatekeeperPasswordHandle(getIntent())) {
mFooterBarMixin.getPrimaryButton().setEnabled(false); if (generateChallengeOnCreate()) {
// We either block on generateChallenge, or need to gray out the "next" button until mFooterBarMixin.getPrimaryButton().setEnabled(false);
// the challenge is ready. Let's just do this for now. // We either block on generateChallenge, or need to gray out the "next" button until
mFaceManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> { // the challenge is ready. Let's just do this for now.
mToken = BiometricUtils.requestGatekeeperHat(this, getIntent(), mUserId, challenge); mFaceManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> {
mSensorId = sensorId; mToken = BiometricUtils.requestGatekeeperHat(this, getIntent(), mUserId,
mChallenge = challenge; challenge);
mFooterBarMixin.getPrimaryButton().setEnabled(true); mSensorId = sensorId;
}); mChallenge = challenge;
mFooterBarMixin.getPrimaryButton().setEnabled(true);
});
}
} }
} }
protected boolean generateChallengeOnCreate() {
return true;
}
@Override @Override
protected boolean isDisabledByAdmin() { protected boolean isDisabledByAdmin() {
return RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( return RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled(

View File

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

View File

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

View File

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