Fixes: 169629017
Test: Wipe device, go through setup flow with a managed account.
Successfully set up credential and fingerprint
During the conversion to GkPwHandle (instead of HAT), the code in
Choose/ConfirmLock* and most of the biometric paths were updated, with
the exception of 2a below.
1) Only multi-biometric devices request Choose/ConfirmLock in
BiometricEnrollActivity.
2) Single-biometric devices (in almost all paths) request credentials
in their intro activities (FingerprintEnrollIntro, etc).
2a) However, there is a special path used by work profiles where
credentials are first set up, and the GkPwHandle is passed into
BiometricEnrollActivity, with the request to skip the fingerprint
enroll introduction page. In this case, we must remember to
forward the GkPwHandle to the relavent enrollment page
(FingerprintEnrollFindSensor).
At some point in the future we should have all credential stuff
done in BiometricEnrollActivity. However, due to legacy APIs, etc,
it may be more work than it's worth right now.
Change-Id: I3f95876de6969fee7130d7a19c8db363da69c4c2
406 lines
17 KiB
Java
406 lines
17 KiB
Java
/*
|
|
* Copyright (C) 2018 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License
|
|
*/
|
|
|
|
package com.android.settings.biometrics;
|
|
|
|
import android.annotation.NonNull;
|
|
import android.app.admin.DevicePolicyManager;
|
|
import android.app.settings.SettingsEnums;
|
|
import android.content.Intent;
|
|
import android.content.pm.PackageManager;
|
|
import android.content.res.Resources;
|
|
import android.hardware.biometrics.BiometricManager;
|
|
import android.hardware.biometrics.BiometricManager.Authenticators;
|
|
import android.hardware.face.FaceManager;
|
|
import android.hardware.face.FaceSensorPropertiesInternal;
|
|
import android.hardware.fingerprint.FingerprintManager;
|
|
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
|
|
import android.os.Bundle;
|
|
import android.os.UserHandle;
|
|
import android.os.UserManager;
|
|
import android.provider.Settings;
|
|
import android.util.Log;
|
|
import android.view.View;
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
import com.android.internal.widget.LockPatternUtils;
|
|
import com.android.settings.R;
|
|
import com.android.settings.SetupWizardUtils;
|
|
import com.android.settings.core.InstrumentedActivity;
|
|
import com.android.settings.password.ChooseLockGeneric;
|
|
import com.android.settings.password.ChooseLockPattern;
|
|
import com.android.settings.password.ChooseLockSettingsHelper;
|
|
|
|
import com.google.android.setupcompat.template.FooterBarMixin;
|
|
import com.google.android.setupcompat.template.FooterButton;
|
|
import com.google.android.setupcompat.util.WizardManagerHelper;
|
|
import com.google.android.setupdesign.GlifLayout;
|
|
|
|
import java.util.List;
|
|
|
|
/**
|
|
* Trampoline activity launched by the {@code android.settings.BIOMETRIC_ENROLL} action which
|
|
* shows the user an appropriate enrollment flow depending on the device's biometric hardware.
|
|
* This activity must only allow enrollment of biometrics that can be used by
|
|
* {@link android.hardware.biometrics.BiometricPrompt}.
|
|
*/
|
|
public class BiometricEnrollActivity extends InstrumentedActivity {
|
|
|
|
private static final String TAG = "BiometricEnrollActivity";
|
|
|
|
private static final int REQUEST_CHOOSE_LOCK = 1;
|
|
private static final int REQUEST_CONFIRM_LOCK = 2;
|
|
|
|
public static final int RESULT_SKIP = BiometricEnrollBase.RESULT_SKIP;
|
|
|
|
// Intent extra. If true, biometric enrollment should skip introductory screens. Currently
|
|
// this only applies to fingerprint.
|
|
public static final String EXTRA_SKIP_INTRO = "skip_intro";
|
|
|
|
private static final String SAVED_STATE_CONFIRMING_CREDENTIALS = "confirming_credentials";
|
|
private static final String SAVED_STATE_GK_PW_HANDLE = "gk_pw_handle";
|
|
|
|
public static final class InternalActivity extends BiometricEnrollActivity {}
|
|
|
|
private int mUserId = UserHandle.myUserId();
|
|
private boolean mConfirmingCredentials;
|
|
@Nullable private Long mGkPwHandle;
|
|
private BiometricEnrollCheckbox mCheckboxFace;
|
|
private BiometricEnrollCheckbox mCheckboxFingerprint;
|
|
@Nullable private MultiBiometricEnrollHelper mMultiBiometricEnrollHelper;
|
|
|
|
@Override
|
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
|
super.onCreate(savedInstanceState);
|
|
|
|
if (this instanceof InternalActivity) {
|
|
mUserId = getIntent().getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId());
|
|
if (BiometricUtils.containsGatekeeperPasswordHandle(getIntent())) {
|
|
mGkPwHandle = BiometricUtils.getGatekeeperPasswordHandle(getIntent());
|
|
}
|
|
}
|
|
|
|
if (savedInstanceState != null) {
|
|
mConfirmingCredentials = savedInstanceState
|
|
.getBoolean(SAVED_STATE_CONFIRMING_CREDENTIALS, false);
|
|
if (savedInstanceState.containsKey(SAVED_STATE_GK_PW_HANDLE)) {
|
|
mGkPwHandle = savedInstanceState.getLong(SAVED_STATE_GK_PW_HANDLE);
|
|
}
|
|
}
|
|
|
|
// Put the theme in the intent so it gets propagated to other activities in the flow
|
|
final Intent intent = getIntent();
|
|
if (intent.getStringExtra(WizardManagerHelper.EXTRA_THEME) == null) {
|
|
intent.putExtra(
|
|
WizardManagerHelper.EXTRA_THEME,
|
|
SetupWizardUtils.getThemeString(intent));
|
|
}
|
|
|
|
// Default behavior is to enroll BIOMETRIC_WEAK or above. See ACTION_BIOMETRIC_ENROLL.
|
|
final int authenticators = intent.getIntExtra(
|
|
Settings.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());
|
|
|
|
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.
|
|
final BiometricManager bm = getSystemService(BiometricManager.class);
|
|
final @BiometricManager.BiometricError int result = bm.canAuthenticate(authenticators);
|
|
if (result != BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
|
Log.e(TAG, "Unexpected result: " + 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();
|
|
} else {
|
|
Log.e(TAG, "Unknown state, finishing");
|
|
finish();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
|
super.onSaveInstanceState(outState);
|
|
outState.putBoolean(SAVED_STATE_CONFIRMING_CREDENTIALS, mConfirmingCredentials);
|
|
if (mGkPwHandle != null) {
|
|
outState.putLong(SAVED_STATE_GK_PW_HANDLE, mGkPwHandle);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
super.onActivityResult(requestCode, resultCode, data);
|
|
|
|
if (mMultiBiometricEnrollHelper == null) {
|
|
overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
|
|
|
|
switch (requestCode) {
|
|
case REQUEST_CHOOSE_LOCK:
|
|
mConfirmingCredentials = false;
|
|
if (resultCode == ChooseLockPattern.RESULT_FINISHED) {
|
|
mGkPwHandle = BiometricUtils.getGatekeeperPasswordHandle(data);
|
|
} else {
|
|
Log.d(TAG, "Unknown result for chooseLock: " + resultCode);
|
|
setResult(resultCode);
|
|
finish();
|
|
}
|
|
break;
|
|
case REQUEST_CONFIRM_LOCK:
|
|
mConfirmingCredentials = false;
|
|
if (resultCode == RESULT_OK) {
|
|
mGkPwHandle = BiometricUtils.getGatekeeperPasswordHandle(data);
|
|
} else {
|
|
Log.d(TAG, "Unknown result for confirmLock: " + resultCode);
|
|
finish();
|
|
}
|
|
break;
|
|
default:
|
|
Log.d(TAG, "Unknown requestCode: " + requestCode + ", finishing");
|
|
finish();
|
|
}
|
|
} else {
|
|
mMultiBiometricEnrollHelper.onActivityResult(requestCode, resultCode, data);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {
|
|
final int new_resid = SetupWizardUtils.getTheme(this, getIntent());
|
|
theme.applyStyle(R.style.SetupWizardPartnerResource, true);
|
|
super.onApplyThemeResource(theme, new_resid, first);
|
|
}
|
|
|
|
@Override
|
|
protected void onStop() {
|
|
super.onStop();
|
|
|
|
if (mConfirmingCredentials || mMultiBiometricEnrollHelper != null) {
|
|
return;
|
|
}
|
|
|
|
if (!isChangingConfigurations()) {
|
|
Log.d(TAG, "Finishing in onStop");
|
|
finish();
|
|
}
|
|
}
|
|
|
|
private void setupForMultiBiometricEnroll() {
|
|
setContentView(R.layout.biometric_enroll_layout);
|
|
|
|
mCheckboxFace = findViewById(R.id.checkbox_face);
|
|
mCheckboxFingerprint = findViewById(R.id.checkbox_fingerprint);
|
|
|
|
mCheckboxFace.setListener(this::updateNextButton);
|
|
mCheckboxFingerprint.setListener(this::updateNextButton);
|
|
|
|
final FingerprintManager fingerprintManager = getSystemService(FingerprintManager.class);
|
|
final FaceManager faceManager = getSystemService(FaceManager.class);
|
|
final List<FingerprintSensorPropertiesInternal> fpProperties =
|
|
fingerprintManager.getSensorPropertiesInternal();
|
|
final List<FaceSensorPropertiesInternal> faceProperties =
|
|
faceManager.getSensorPropertiesInternal();
|
|
|
|
// This would need to be updated for devices with multiple sensors of the same modality
|
|
final boolean maxFacesEnrolled = faceManager.getEnrolledFaces(mUserId).size()
|
|
>= faceProperties.get(0).maxEnrollmentsPerUser;
|
|
final boolean maxFingerprintsEnrolled = fingerprintManager.getEnrolledFingerprints(mUserId)
|
|
.size() >= fpProperties.get(0).maxEnrollmentsPerUser;
|
|
|
|
if (maxFacesEnrolled) {
|
|
mCheckboxFace.setEnabled(false);
|
|
mCheckboxFace.setDescription(R.string.face_intro_error_max);
|
|
}
|
|
|
|
if (maxFingerprintsEnrolled) {
|
|
mCheckboxFingerprint.setEnabled(false);
|
|
mCheckboxFingerprint.setDescription(R.string.fingerprint_intro_error_max);
|
|
}
|
|
|
|
final FooterBarMixin footerBarMixin = ((GlifLayout) findViewById(R.id.setup_wizard_layout))
|
|
.getMixin(FooterBarMixin.class);
|
|
footerBarMixin.setSecondaryButton(new FooterButton.Builder(this)
|
|
.setText(R.string.multi_biometric_enroll_skip)
|
|
.setListener(this::onButtonNegative)
|
|
.setButtonType(FooterButton.ButtonType.SKIP)
|
|
.setTheme(R.style.SudGlifButton_Secondary)
|
|
.build());
|
|
|
|
footerBarMixin.setPrimaryButton(new FooterButton.Builder(this)
|
|
.setText(R.string.multi_biometric_enroll_next)
|
|
.setListener(this::onButtonPositive)
|
|
.setButtonType(FooterButton.ButtonType.NEXT)
|
|
.setTheme(R.style.SudGlifButton_Primary)
|
|
.build());
|
|
|
|
footerBarMixin.getSecondaryButton().setVisibility(View.VISIBLE);
|
|
footerBarMixin.getPrimaryButton().setVisibility(View.VISIBLE);
|
|
|
|
if (!mConfirmingCredentials && mGkPwHandle == null) {
|
|
mConfirmingCredentials = true;
|
|
if (!userHasPassword(mUserId)) {
|
|
launchChooseLock();
|
|
} else {
|
|
launchConfirmLock();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void updateNextButton(View view) {
|
|
final boolean canEnrollAny = canEnrollFace() || canEnrollFingerprint();
|
|
|
|
final FooterBarMixin footerBarMixin = ((GlifLayout) findViewById(R.id.setup_wizard_layout))
|
|
.getMixin(FooterBarMixin.class);
|
|
footerBarMixin.getPrimaryButton().setEnabled(canEnrollAny);
|
|
}
|
|
|
|
private void onButtonPositive(View view) {
|
|
// Start the state machine according to checkboxes, taking max enrolled into account
|
|
mMultiBiometricEnrollHelper = new MultiBiometricEnrollHelper(this, mUserId,
|
|
canEnrollFace(), canEnrollFingerprint(), mGkPwHandle);
|
|
mMultiBiometricEnrollHelper.startNextStep();
|
|
}
|
|
|
|
private void onButtonNegative(View view) {
|
|
setResult(RESULT_SKIP);
|
|
finish();
|
|
}
|
|
|
|
private boolean canEnrollFace() {
|
|
return mCheckboxFace.isEnabled() && mCheckboxFace.isChecked();
|
|
}
|
|
|
|
private boolean canEnrollFingerprint() {
|
|
return mCheckboxFingerprint.isEnabled() && mCheckboxFingerprint.isChecked();
|
|
}
|
|
|
|
private boolean userHasPassword(int userId) {
|
|
final UserManager userManager = getSystemService(UserManager.class);
|
|
final int passwordQuality = new LockPatternUtils(this)
|
|
.getActivePasswordQuality(userManager.getCredentialOwnerProfile(userId));
|
|
return passwordQuality != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
|
|
}
|
|
|
|
private void launchChooseLock() {
|
|
Log.d(TAG, "launchChooseLock");
|
|
Intent intent = BiometricUtils.getChooseLockIntent(this, getIntent());
|
|
intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.MINIMUM_QUALITY_KEY,
|
|
DevicePolicyManager.PASSWORD_QUALITY_SOMETHING);
|
|
intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_DISABLED_PREFS, true);
|
|
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true);
|
|
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_BIOMETRICS, true);
|
|
|
|
if (mUserId != UserHandle.USER_NULL) {
|
|
intent.putExtra(Intent.EXTRA_USER_ID, mUserId);
|
|
}
|
|
startActivityForResult(intent, REQUEST_CHOOSE_LOCK);
|
|
}
|
|
|
|
private void launchConfirmLock() {
|
|
Log.d(TAG, "launchConfirmLock");
|
|
final ChooseLockSettingsHelper.Builder builder = new ChooseLockSettingsHelper.Builder(this);
|
|
builder.setRequestCode(REQUEST_CONFIRM_LOCK)
|
|
.setRequestGatekeeperPasswordHandle(true)
|
|
.setForegroundOnly(true)
|
|
.setReturnCredentials(true);
|
|
if (mUserId != UserHandle.USER_NULL) {
|
|
builder.setUserId(mUserId);
|
|
}
|
|
final boolean launched = builder.show();
|
|
if (!launched) {
|
|
// This shouldn't happen, as we should only end up at this step if a lock thingy is
|
|
// already set.
|
|
finish();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This should only be used to launch enrollment for single-sensor devices, which use
|
|
* FLAG_ACTIVITY_FORWARD_RESULT path.
|
|
*
|
|
* @param intent Enrollment activity that should be started (e.g. FaceEnrollIntroduction.class,
|
|
* etc).
|
|
*/
|
|
private void launchEnrollActivity(@NonNull Intent intent) {
|
|
intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
|
|
byte[] hardwareAuthToken = null;
|
|
if (this instanceof InternalActivity) {
|
|
hardwareAuthToken = getIntent().getByteArrayExtra(
|
|
ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
|
|
}
|
|
BiometricUtils.launchEnrollForResult(this, intent, 0 /* requestCode */, hardwareAuthToken,
|
|
mGkPwHandle, mUserId);
|
|
}
|
|
|
|
private void launchCredentialOnlyEnroll() {
|
|
final Intent intent;
|
|
// If only device credential was specified, ask the user to only set that up.
|
|
intent = new Intent(this, ChooseLockGeneric.class);
|
|
intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.MINIMUM_QUALITY_KEY,
|
|
DevicePolicyManager.PASSWORD_QUALITY_SOMETHING);
|
|
launchEnrollActivity(intent);
|
|
}
|
|
|
|
private void launchFingerprintOnlyEnroll() {
|
|
final Intent intent;
|
|
// ChooseLockGeneric can request to start fingerprint enroll bypassing the intro screen.
|
|
if (getIntent().getBooleanExtra(EXTRA_SKIP_INTRO, false)
|
|
&& this instanceof InternalActivity) {
|
|
intent = BiometricUtils.getFingerprintFindSensorIntent(this, getIntent());
|
|
} else {
|
|
intent = BiometricUtils.getFingerprintIntroIntent(this, getIntent());
|
|
}
|
|
launchEnrollActivity(intent);
|
|
}
|
|
|
|
private void launchFaceOnlyEnroll() {
|
|
final Intent intent = BiometricUtils.getFaceIntroIntent(this, getIntent());
|
|
launchEnrollActivity(intent);
|
|
}
|
|
|
|
@Override
|
|
public int getMetricsCategory() {
|
|
return SettingsEnums.BIOMETRIC_ENROLL_ACTIVITY;
|
|
}
|
|
}
|