From a8808f73688ddcf1d6e6061dbab67027435fd224 Mon Sep 17 00:00:00 2001 From: Joe Bolinger Date: Thu, 10 Jun 2021 13:36:38 -0700 Subject: [PATCH] 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 --- AndroidManifest.xml | 5 + res/values/strings.xml | 6 +- .../biometrics/BiometricEnrollActivity.java | 280 ++++++++++++------ .../biometrics/BiometricEnrollBase.java | 21 ++ .../BiometricEnrollIntroduction.java | 56 ++-- .../biometrics/ParentalConsentHelper.java | 168 +++++++++++ .../face/FaceEnrollIntroduction.java | 26 +- .../face/FaceEnrollParentalConsent.java | 67 +++++ .../FingerprintEnrollParentalConsent.java | 62 ++++ .../biometrics/ParentalConsentHelperTest.java | 224 ++++++++++++++ 10 files changed, 801 insertions(+), 114 deletions(-) create mode 100644 src/com/android/settings/biometrics/ParentalConsentHelper.java create mode 100644 src/com/android/settings/biometrics/face/FaceEnrollParentalConsent.java create mode 100644 src/com/android/settings/biometrics/fingerprint/FingerprintEnrollParentalConsent.java create mode 100644 tests/unit/src/com/android/settings/biometrics/ParentalConsentHelperTest.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 65a6f045cc1..6f6482daeb5 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1803,6 +1803,10 @@ android:theme="@style/GlifV3Theme.Light" android:exported="false"/> + + @@ -1837,6 +1841,7 @@ + diff --git a/res/values/strings.xml b/res/values/strings.xml index 3b4fae4b944..0302461559c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -777,6 +777,8 @@ More Unlock with your face + + Allow face unlock Use your face to authenticate @@ -888,8 +890,10 @@ - + Set up your fingerprint + + Allow fingerprint unlock Use your fingerprint diff --git a/src/com/android/settings/biometrics/BiometricEnrollActivity.java b/src/com/android/settings/biometrics/BiometricEnrollActivity.java index ef01a4bfb7e..6ab9ab8828c 100644 --- a/src/com/android/settings/biometrics/BiometricEnrollActivity.java +++ b/src/com/android/settings/biometrics/BiometricEnrollActivity.java @@ -19,6 +19,9 @@ package com.android.settings.biometrics; 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 @@ public class BiometricEnrollActivity extends InstrumentedActivity { 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 @@ public class BiometricEnrollActivity extends InstrumentedActivity { // 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 @@ public class BiometricEnrollActivity extends InstrumentedActivity { 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 @@ public class BiometricEnrollActivity extends InstrumentedActivity { 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 @@ public class BiometricEnrollActivity extends InstrumentedActivity { 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 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 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 @@ public class BiometricEnrollActivity extends InstrumentedActivity { 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 @@ public class BiometricEnrollActivity extends InstrumentedActivity { 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 @@ 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 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 @@ public class BiometricEnrollActivity extends InstrumentedActivity { } } - private void setupForMultiBiometricEnroll() { + + private void setOrConfirmCredentialsNow() { if (!mConfirmingCredentials) { mConfirmingCredentials = true; if (!userHasPassword(mUserId)) { @@ -268,37 +396,11 @@ public class BiometricEnrollActivity extends InstrumentedActivity { } } - 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 fpProperties = - fingerprintManager.getSensorPropertiesInternal(); - final List 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 @@ public class BiometricEnrollActivity extends InstrumentedActivity { 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 @@ public class BiometricEnrollActivity extends InstrumentedActivity { 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 @@ public class BiometricEnrollActivity extends InstrumentedActivity { * @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 @@ public class BiometricEnrollActivity extends InstrumentedActivity { // 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 @@ public class BiometricEnrollActivity extends InstrumentedActivity { } 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 b62b35fe201..6e7d04f26b5 100644 --- a/src/com/android/settings/biometrics/BiometricEnrollBase.java +++ b/src/com/android/settings/biometrics/BiometricEnrollBase.java @@ -26,6 +26,7 @@ import android.graphics.Color; 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 @@ import com.google.android.setupdesign.util.ThemeHelper; */ 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 abstract class BiometricEnrollBase extends InstrumentedActivity { */ 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 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity { } 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 580c753bd32..c073c3c2eac 100644 --- a/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java +++ b/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java @@ -294,15 +294,19 @@ public abstract class BiometricEnrollIntroduction extends BiometricEnrollBase 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 @@ public abstract class BiometricEnrollIntroduction extends BiometricEnrollBase } 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 @@ public abstract class BiometricEnrollIntroduction extends BiometricEnrollBase 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 00000000000..905a95527ef --- /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 8e733b4da89..fc13a8c6161 100644 --- a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java +++ b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java @@ -85,22 +85,28 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction { 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 00000000000..8e80b3958c5 --- /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 00000000000..8920307cfa8 --- /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 00000000000..78856da7692 --- /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 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> 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 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; + } +}