Enforce a lock screen that adheres with the required complexity set by the admin. This is done by querying the DevicePolicyManager for the complexity set for the given user, and merging it with the complexity from the "change lock screen" intent (if any). If the admin sets a higher complexity requirement than the app triggering the lock screen change request, then the admin-set complexity is enforced and the user is not shown information about the requesting app. Bug: 165573442 Test: Manually, set complexity using TestDPC and see it applies. Test: m RunSettingsRoboTests ROBOTEST_FILTER=com.android.settings.password.ChooseLockGenericTest Test: m RunSettingsRoboTests ROBOTEST_FILTER=com.android.settings.password.ChooseLockPasswordTest Change-Id: If3f24f7430bdcbcd34265339f7d2a1ff82a44fc1
1027 lines
45 KiB
Java
1027 lines
45 KiB
Java
/*
|
|
* Copyright (C) 2010 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.password;
|
|
|
|
import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE;
|
|
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
|
|
|
|
import static com.android.internal.widget.PasswordValidationError.CONTAINS_INVALID_CHARACTERS;
|
|
import static com.android.internal.widget.PasswordValidationError.CONTAINS_SEQUENCE;
|
|
import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_DIGITS;
|
|
import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_LETTERS;
|
|
import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_LOWER_CASE;
|
|
import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_NON_DIGITS;
|
|
import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_NON_LETTER;
|
|
import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_SYMBOLS;
|
|
import static com.android.internal.widget.PasswordValidationError.NOT_ENOUGH_UPPER_CASE;
|
|
import static com.android.internal.widget.PasswordValidationError.RECENTLY_USED;
|
|
import static com.android.internal.widget.PasswordValidationError.TOO_LONG;
|
|
import static com.android.internal.widget.PasswordValidationError.TOO_SHORT;
|
|
import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_REQUESTED_MIN_COMPLEXITY;
|
|
import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_UNIFICATION_PROFILE_CREDENTIAL;
|
|
import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_UNIFICATION_PROFILE_ID;
|
|
|
|
import android.app.Activity;
|
|
import android.app.admin.DevicePolicyManager;
|
|
import android.app.admin.DevicePolicyManager.PasswordComplexity;
|
|
import android.app.admin.PasswordMetrics;
|
|
import android.app.settings.SettingsEnums;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.res.Resources.Theme;
|
|
import android.graphics.Insets;
|
|
import android.graphics.Typeface;
|
|
import android.os.Bundle;
|
|
import android.os.Handler;
|
|
import android.os.Message;
|
|
import android.os.UserHandle;
|
|
import android.text.Editable;
|
|
import android.text.InputType;
|
|
import android.text.Selection;
|
|
import android.text.Spannable;
|
|
import android.text.TextUtils;
|
|
import android.text.TextWatcher;
|
|
import android.util.Log;
|
|
import android.util.Pair;
|
|
import android.view.KeyEvent;
|
|
import android.view.LayoutInflater;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.inputmethod.EditorInfo;
|
|
import android.widget.ImeAwareEditText;
|
|
import android.widget.TextView;
|
|
import android.widget.TextView.OnEditorActionListener;
|
|
|
|
import androidx.annotation.StringRes;
|
|
import androidx.fragment.app.Fragment;
|
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
import androidx.recyclerview.widget.RecyclerView;
|
|
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
import com.android.internal.widget.LockPatternUtils;
|
|
import com.android.internal.widget.LockscreenCredential;
|
|
import com.android.internal.widget.PasswordValidationError;
|
|
import com.android.internal.widget.TextViewInputDisabler;
|
|
import com.android.internal.widget.VerifyCredentialResponse;
|
|
import com.android.settings.EncryptionInterstitial;
|
|
import com.android.settings.R;
|
|
import com.android.settings.SettingsActivity;
|
|
import com.android.settings.SetupWizardUtils;
|
|
import com.android.settings.Utils;
|
|
import com.android.settings.core.InstrumentedFragment;
|
|
import com.android.settings.notification.RedactionInterstitial;
|
|
|
|
import com.google.android.setupcompat.template.FooterBarMixin;
|
|
import com.google.android.setupcompat.template.FooterButton;
|
|
import com.google.android.setupdesign.GlifLayout;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
|
|
public class ChooseLockPassword extends SettingsActivity {
|
|
private static final String TAG = "ChooseLockPassword";
|
|
|
|
@Override
|
|
public Intent getIntent() {
|
|
Intent modIntent = new Intent(super.getIntent());
|
|
modIntent.putExtra(EXTRA_SHOW_FRAGMENT, getFragmentClass().getName());
|
|
return modIntent;
|
|
}
|
|
|
|
@Override
|
|
protected void onApplyThemeResource(Theme theme, int resid, boolean first) {
|
|
final int new_resid = SetupWizardUtils.getTheme(this, getIntent());
|
|
super.onApplyThemeResource(theme, new_resid, first);
|
|
}
|
|
|
|
public static class IntentBuilder {
|
|
|
|
private final Intent mIntent;
|
|
|
|
public IntentBuilder(Context context) {
|
|
mIntent = new Intent(context, ChooseLockPassword.class);
|
|
mIntent.putExtra(ChooseLockGeneric.CONFIRM_CREDENTIALS, false);
|
|
mIntent.putExtra(EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, false);
|
|
}
|
|
|
|
public IntentBuilder setPasswordQuality(int quality) {
|
|
mIntent.putExtra(LockPatternUtils.PASSWORD_TYPE_KEY, quality);
|
|
return this;
|
|
}
|
|
|
|
public IntentBuilder setUserId(int userId) {
|
|
mIntent.putExtra(Intent.EXTRA_USER_ID, userId);
|
|
return this;
|
|
}
|
|
|
|
public IntentBuilder setRequestGatekeeperPasswordHandle(
|
|
boolean requestGatekeeperPasswordHandle) {
|
|
mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE,
|
|
requestGatekeeperPasswordHandle);
|
|
return this;
|
|
}
|
|
|
|
public IntentBuilder setPassword(LockscreenCredential password) {
|
|
mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, password);
|
|
return this;
|
|
}
|
|
|
|
public IntentBuilder setForFingerprint(boolean forFingerprint) {
|
|
mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, forFingerprint);
|
|
return this;
|
|
}
|
|
|
|
public IntentBuilder setForFace(boolean forFace) {
|
|
mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, forFace);
|
|
return this;
|
|
}
|
|
|
|
public IntentBuilder setForBiometrics(boolean forBiometrics) {
|
|
mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_BIOMETRICS, forBiometrics);
|
|
return this;
|
|
}
|
|
|
|
public IntentBuilder setRequestedMinComplexity(@PasswordComplexity int level) {
|
|
mIntent.putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, level);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Configures the launch such that at the end of the password enrollment, one of its
|
|
* managed profile (specified by {@code profileId}) will have its lockscreen unified
|
|
* to the parent user. The profile's current lockscreen credential needs to be specified by
|
|
* {@code credential}.
|
|
*/
|
|
public IntentBuilder setProfileToUnify(int profileId, LockscreenCredential credential) {
|
|
mIntent.putExtra(EXTRA_KEY_UNIFICATION_PROFILE_ID, profileId);
|
|
mIntent.putExtra(EXTRA_KEY_UNIFICATION_PROFILE_CREDENTIAL, credential);
|
|
return this;
|
|
}
|
|
|
|
public Intent build() {
|
|
return mIntent;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected boolean isValidFragment(String fragmentName) {
|
|
if (ChooseLockPasswordFragment.class.getName().equals(fragmentName)) return true;
|
|
return false;
|
|
}
|
|
|
|
/* package */ Class<? extends Fragment> getFragmentClass() {
|
|
return ChooseLockPasswordFragment.class;
|
|
}
|
|
|
|
@Override
|
|
protected void onCreate(Bundle savedInstanceState) {
|
|
super.onCreate(savedInstanceState);
|
|
final boolean forFingerprint = getIntent()
|
|
.getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, false);
|
|
final boolean forFace = getIntent()
|
|
.getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, false);
|
|
final boolean forBiometrics = getIntent()
|
|
.getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_BIOMETRICS, false);
|
|
|
|
CharSequence msg = getText(R.string.lockpassword_choose_your_screen_lock_header);
|
|
if (forFingerprint) {
|
|
msg = getText(R.string.lockpassword_choose_your_password_header_for_fingerprint);
|
|
} else if (forFace) {
|
|
msg = getText(R.string.lockpassword_choose_your_password_header_for_face);
|
|
} else if (forBiometrics) {
|
|
msg = getText(R.string.lockpassword_choose_your_password_header_for_biometrics);
|
|
}
|
|
|
|
setTitle(msg);
|
|
findViewById(R.id.content_parent).setFitsSystemWindows(false);
|
|
}
|
|
|
|
public static class ChooseLockPasswordFragment extends InstrumentedFragment
|
|
implements OnEditorActionListener, TextWatcher, SaveAndFinishWorker.Listener {
|
|
private static final String KEY_FIRST_PASSWORD = "first_password";
|
|
private static final String KEY_UI_STAGE = "ui_stage";
|
|
private static final String KEY_CURRENT_CREDENTIAL = "current_credential";
|
|
private static final String FRAGMENT_TAG_SAVE_AND_FINISH = "save_and_finish_worker";
|
|
|
|
private LockscreenCredential mCurrentCredential;
|
|
private LockscreenCredential mChosenPassword;
|
|
private boolean mRequestGatekeeperPassword;
|
|
private ImeAwareEditText mPasswordEntry;
|
|
private TextViewInputDisabler mPasswordEntryInputDisabler;
|
|
|
|
// Minimum password metrics enforced by admins.
|
|
private PasswordMetrics mMinMetrics;
|
|
private List<PasswordValidationError> mValidationErrors;
|
|
|
|
@PasswordComplexity private int mMinComplexity = PASSWORD_COMPLEXITY_NONE;
|
|
protected int mUserId;
|
|
private byte[] mPasswordHistoryHashFactor;
|
|
private int mUnificationProfileId = UserHandle.USER_NULL;
|
|
|
|
private LockPatternUtils mLockPatternUtils;
|
|
private SaveAndFinishWorker mSaveAndFinishWorker;
|
|
private int mRequestedQuality = DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
|
|
protected Stage mUiStage = Stage.Introduction;
|
|
private PasswordRequirementAdapter mPasswordRequirementAdapter;
|
|
private GlifLayout mLayout;
|
|
protected boolean mForFingerprint;
|
|
protected boolean mForFace;
|
|
protected boolean mForBiometrics;
|
|
|
|
private LockscreenCredential mFirstPassword;
|
|
private RecyclerView mPasswordRestrictionView;
|
|
protected boolean mIsAlphaMode;
|
|
protected FooterButton mSkipOrClearButton;
|
|
private FooterButton mNextButton;
|
|
private TextView mMessage;
|
|
|
|
private TextChangedHandler mTextChangedHandler;
|
|
|
|
private static final int CONFIRM_EXISTING_REQUEST = 58;
|
|
static final int RESULT_FINISHED = RESULT_FIRST_USER;
|
|
|
|
/**
|
|
* Keep track internally of where the user is in choosing a pattern.
|
|
*/
|
|
protected enum Stage {
|
|
|
|
Introduction(
|
|
R.string.lockpassword_choose_your_screen_lock_header, // password
|
|
R.string.lockpassword_choose_your_password_header_for_fingerprint,
|
|
R.string.lockpassword_choose_your_password_header_for_face,
|
|
R.string.lockpassword_choose_your_password_header_for_biometrics,
|
|
R.string.lockpassword_choose_your_screen_lock_header, // pin
|
|
R.string.lockpassword_choose_your_pin_header_for_fingerprint,
|
|
R.string.lockpassword_choose_your_pin_header_for_face,
|
|
R.string.lockpassword_choose_your_pin_header_for_biometrics,
|
|
R.string.lockpassword_choose_your_password_message, // added security message
|
|
R.string.lock_settings_picker_biometrics_added_security_message,
|
|
R.string.lockpassword_choose_your_pin_message,
|
|
R.string.lock_settings_picker_biometrics_added_security_message,
|
|
R.string.next_label),
|
|
|
|
NeedToConfirm(
|
|
R.string.lockpassword_confirm_your_password_header,
|
|
R.string.lockpassword_confirm_your_password_header,
|
|
R.string.lockpassword_confirm_your_password_header,
|
|
R.string.lockpassword_confirm_your_password_header,
|
|
R.string.lockpassword_confirm_your_pin_header,
|
|
R.string.lockpassword_confirm_your_pin_header,
|
|
R.string.lockpassword_confirm_your_pin_header,
|
|
R.string.lockpassword_confirm_your_pin_header,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
R.string.lockpassword_confirm_label),
|
|
|
|
ConfirmWrong(
|
|
R.string.lockpassword_confirm_passwords_dont_match,
|
|
R.string.lockpassword_confirm_passwords_dont_match,
|
|
R.string.lockpassword_confirm_passwords_dont_match,
|
|
R.string.lockpassword_confirm_passwords_dont_match,
|
|
R.string.lockpassword_confirm_pins_dont_match,
|
|
R.string.lockpassword_confirm_pins_dont_match,
|
|
R.string.lockpassword_confirm_pins_dont_match,
|
|
R.string.lockpassword_confirm_pins_dont_match,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
R.string.lockpassword_confirm_label);
|
|
|
|
Stage(int hintInAlpha,
|
|
int hintInAlphaForFingerprint, int hintInAlphaForFace, int hintInAlphaForBiometrics,
|
|
int hintInNumeric,
|
|
int hintInNumericForFingerprint, int hintInNumericForFace, int hintInNumericForBiometrics,
|
|
int messageInAlpha, int messageInAlphaForBiometrics,
|
|
int messageInNumeric, int messageInNumericForBiometrics,
|
|
int nextButtonText) {
|
|
this.alphaHint = hintInAlpha;
|
|
this.alphaHintForFingerprint = hintInAlphaForFingerprint;
|
|
this.alphaHintForFace = hintInAlphaForFace;
|
|
this.alphaHintForBiometrics = hintInAlphaForBiometrics;
|
|
|
|
this.numericHint = hintInNumeric;
|
|
this.numericHintForFingerprint = hintInNumericForFingerprint;
|
|
this.numericHintForFace = hintInNumericForFace;
|
|
this.numericHintForBiometrics = hintInNumericForBiometrics;
|
|
|
|
this.alphaMessage = messageInAlpha;
|
|
this.alphaMessageForBiometrics = messageInAlphaForBiometrics;
|
|
this.numericMessage = messageInNumeric;
|
|
this.numericMessageForBiometrics = messageInNumericForBiometrics;
|
|
this.buttonText = nextButtonText;
|
|
}
|
|
|
|
public static final int TYPE_NONE = 0;
|
|
public static final int TYPE_FINGERPRINT = 1;
|
|
public static final int TYPE_FACE = 2;
|
|
public static final int TYPE_BIOMETRIC = 3;
|
|
|
|
// Password
|
|
public final int alphaHint;
|
|
public final int alphaHintForFingerprint;
|
|
public final int alphaHintForFace;
|
|
public final int alphaHintForBiometrics;
|
|
|
|
// PIN
|
|
public final int numericHint;
|
|
public final int numericHintForFingerprint;
|
|
public final int numericHintForFace;
|
|
public final int numericHintForBiometrics;
|
|
|
|
public final int alphaMessage;
|
|
public final int alphaMessageForBiometrics;
|
|
public final int numericMessage;
|
|
public final int numericMessageForBiometrics;
|
|
public final int buttonText;
|
|
|
|
public @StringRes int getHint(boolean isAlpha, int type) {
|
|
if (isAlpha) {
|
|
if (type == TYPE_FINGERPRINT) {
|
|
return alphaHintForFingerprint;
|
|
} else if (type == TYPE_FACE) {
|
|
return alphaHintForFace;
|
|
} else if (type == TYPE_BIOMETRIC) {
|
|
return alphaHintForBiometrics;
|
|
} else {
|
|
return alphaHint;
|
|
}
|
|
} else {
|
|
if (type == TYPE_FINGERPRINT) {
|
|
return numericHintForFingerprint;
|
|
} else if (type == TYPE_FACE) {
|
|
return numericHintForFace;
|
|
} else if (type == TYPE_BIOMETRIC) {
|
|
return numericHintForBiometrics;
|
|
} else {
|
|
return numericHint;
|
|
}
|
|
}
|
|
}
|
|
|
|
public @StringRes int getMessage(boolean isAlpha, int type) {
|
|
if (isAlpha) {
|
|
return type != TYPE_NONE ? alphaMessageForBiometrics : alphaMessage;
|
|
} else {
|
|
return type != TYPE_NONE ? numericMessageForBiometrics : numericMessage;
|
|
}
|
|
}
|
|
}
|
|
|
|
// required constructor for fragments
|
|
public ChooseLockPasswordFragment() {
|
|
|
|
}
|
|
|
|
@Override
|
|
public void onCreate(Bundle savedInstanceState) {
|
|
super.onCreate(savedInstanceState);
|
|
mLockPatternUtils = new LockPatternUtils(getActivity());
|
|
Intent intent = getActivity().getIntent();
|
|
if (!(getActivity() instanceof ChooseLockPassword)) {
|
|
throw new SecurityException("Fragment contained in wrong activity");
|
|
}
|
|
// Only take this argument into account if it belongs to the current profile.
|
|
mUserId = Utils.getUserIdFromBundle(getActivity(), intent.getExtras());
|
|
mForFingerprint = intent.getBooleanExtra(
|
|
ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, false);
|
|
mForFace = intent.getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, false);
|
|
mForBiometrics = intent.getBooleanExtra(
|
|
ChooseLockSettingsHelper.EXTRA_KEY_FOR_BIOMETRICS, false);
|
|
mMinComplexity = intent.getIntExtra(
|
|
EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_NONE);
|
|
|
|
mRequestedQuality = intent.getIntExtra(
|
|
LockPatternUtils.PASSWORD_TYPE_KEY, PASSWORD_QUALITY_NUMERIC);
|
|
mUnificationProfileId = intent.getIntExtra(
|
|
EXTRA_KEY_UNIFICATION_PROFILE_ID, UserHandle.USER_NULL);
|
|
|
|
mMinMetrics = mLockPatternUtils.getRequestedPasswordMetrics(mUserId);
|
|
// If we are to unify a work challenge at the end of the credential enrollment, manually
|
|
// merge any password policy from that profile here, so we are enrolling a compliant
|
|
// password. This is because once unified, the profile's password policy will
|
|
// be enforced on the new credential.
|
|
if (mUnificationProfileId != UserHandle.USER_NULL) {
|
|
mMinMetrics.maxWith(
|
|
mLockPatternUtils.getRequestedPasswordMetrics(mUnificationProfileId));
|
|
mMinComplexity = Math.max(mMinComplexity,
|
|
mLockPatternUtils.getRequestedPasswordComplexity(mUnificationProfileId));
|
|
}
|
|
|
|
if (intent.getBooleanExtra(
|
|
ChooseLockSettingsHelper.EXTRA_KEY_FOR_CHANGE_CRED_REQUIRED_FOR_BOOT, false)) {
|
|
SaveAndFinishWorker w = new SaveAndFinishWorker();
|
|
final boolean required = getActivity().getIntent().getBooleanExtra(
|
|
EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true);
|
|
LockscreenCredential currentCredential = intent.getParcelableExtra(
|
|
ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD);
|
|
|
|
final LockPatternUtils utils = new LockPatternUtils(getActivity());
|
|
|
|
w.setBlocking(true);
|
|
w.setListener(this);
|
|
w.start(utils, required, false /* requestGatekeeperPassword */, currentCredential,
|
|
currentCredential, mUserId);
|
|
}
|
|
mTextChangedHandler = new TextChangedHandler();
|
|
}
|
|
|
|
@Override
|
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
|
Bundle savedInstanceState) {
|
|
return inflater.inflate(R.layout.choose_lock_password, container, false);
|
|
}
|
|
|
|
@Override
|
|
public void onViewCreated(View view, Bundle savedInstanceState) {
|
|
super.onViewCreated(view, savedInstanceState);
|
|
|
|
mLayout = (GlifLayout) view;
|
|
|
|
// Make the password container consume the optical insets so the edit text is aligned
|
|
// with the sides of the parent visually.
|
|
ViewGroup container = view.findViewById(R.id.password_container);
|
|
container.setOpticalInsets(Insets.NONE);
|
|
|
|
final FooterBarMixin mixin = mLayout.getMixin(FooterBarMixin.class);
|
|
mixin.setSecondaryButton(
|
|
new FooterButton.Builder(getActivity())
|
|
.setText(R.string.lockpassword_clear_label)
|
|
.setListener(this::onSkipOrClearButtonClick)
|
|
.setButtonType(FooterButton.ButtonType.SKIP)
|
|
.setTheme(R.style.SudGlifButton_Secondary)
|
|
.build()
|
|
);
|
|
mixin.setPrimaryButton(
|
|
new FooterButton.Builder(getActivity())
|
|
.setText(R.string.next_label)
|
|
.setListener(this::onNextButtonClick)
|
|
.setButtonType(FooterButton.ButtonType.NEXT)
|
|
.setTheme(R.style.SudGlifButton_Primary)
|
|
.build()
|
|
);
|
|
mSkipOrClearButton = mixin.getSecondaryButton();
|
|
mNextButton = mixin.getPrimaryButton();
|
|
|
|
mMessage = view.findViewById(R.id.sud_layout_description);
|
|
if (mForFingerprint) {
|
|
mLayout.setIcon(getActivity().getDrawable(R.drawable.ic_fingerprint_header));
|
|
} else if (mForFace) {
|
|
mLayout.setIcon(getActivity().getDrawable(R.drawable.ic_face_header));
|
|
} else if (mForBiometrics) {
|
|
mLayout.setIcon(getActivity().getDrawable(R.drawable.ic_lock));
|
|
}
|
|
|
|
mIsAlphaMode = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == mRequestedQuality
|
|
|| DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == mRequestedQuality
|
|
|| DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == mRequestedQuality;
|
|
|
|
setupPasswordRequirementsView(view);
|
|
|
|
mPasswordRestrictionView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
|
mPasswordEntry = view.findViewById(R.id.password_entry);
|
|
mPasswordEntry.setOnEditorActionListener(this);
|
|
mPasswordEntry.addTextChangedListener(this);
|
|
mPasswordEntry.requestFocus();
|
|
mPasswordEntryInputDisabler = new TextViewInputDisabler(mPasswordEntry);
|
|
|
|
final Activity activity = getActivity();
|
|
|
|
int currentType = mPasswordEntry.getInputType();
|
|
mPasswordEntry.setInputType(mIsAlphaMode ? currentType
|
|
: (InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD));
|
|
if (mIsAlphaMode) {
|
|
mPasswordEntry.setContentDescription(
|
|
getString(R.string.unlock_set_unlock_password_title));
|
|
} else {
|
|
mPasswordEntry.setContentDescription(
|
|
getString(R.string.unlock_set_unlock_pin_title));
|
|
}
|
|
// Can't set via XML since setInputType resets the fontFamily to null
|
|
mPasswordEntry.setTypeface(Typeface.create(
|
|
getContext().getString(com.android.internal.R.string.config_headlineFontFamily),
|
|
Typeface.NORMAL));
|
|
|
|
Intent intent = getActivity().getIntent();
|
|
final boolean confirmCredentials = intent.getBooleanExtra(
|
|
ChooseLockGeneric.CONFIRM_CREDENTIALS, true);
|
|
mCurrentCredential = intent.getParcelableExtra(
|
|
ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD);
|
|
mRequestGatekeeperPassword = intent.getBooleanExtra(
|
|
ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, false);
|
|
if (savedInstanceState == null) {
|
|
updateStage(Stage.Introduction);
|
|
if (confirmCredentials) {
|
|
final ChooseLockSettingsHelper.Builder builder =
|
|
new ChooseLockSettingsHelper.Builder(getActivity());
|
|
builder.setRequestCode(CONFIRM_EXISTING_REQUEST)
|
|
.setTitle(getString(R.string.unlock_set_unlock_launch_picker_title))
|
|
.setReturnCredentials(true)
|
|
.setRequestGatekeeperPasswordHandle(mRequestGatekeeperPassword)
|
|
.setUserId(mUserId)
|
|
.show();
|
|
}
|
|
} else {
|
|
|
|
// restore from previous state
|
|
mFirstPassword = savedInstanceState.getParcelable(KEY_FIRST_PASSWORD);
|
|
final String state = savedInstanceState.getString(KEY_UI_STAGE);
|
|
if (state != null) {
|
|
mUiStage = Stage.valueOf(state);
|
|
updateStage(mUiStage);
|
|
}
|
|
|
|
if (mCurrentCredential == null) {
|
|
mCurrentCredential = savedInstanceState.getParcelable(KEY_CURRENT_CREDENTIAL);
|
|
}
|
|
|
|
// Re-attach to the exiting worker if there is one.
|
|
mSaveAndFinishWorker = (SaveAndFinishWorker) getFragmentManager().findFragmentByTag(
|
|
FRAGMENT_TAG_SAVE_AND_FINISH);
|
|
}
|
|
|
|
if (activity instanceof SettingsActivity) {
|
|
final SettingsActivity sa = (SettingsActivity) activity;
|
|
int title = Stage.Introduction.getHint(mIsAlphaMode, getStageType());
|
|
sa.setTitle(title);
|
|
mLayout.setHeaderText(title);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
super.onDestroy();
|
|
if (mCurrentCredential != null) {
|
|
mCurrentCredential.zeroize();
|
|
}
|
|
// Force a garbage collection immediately to remove remnant of user password shards
|
|
// from memory.
|
|
System.gc();
|
|
System.runFinalization();
|
|
System.gc();
|
|
}
|
|
|
|
protected int getStageType() {
|
|
if (mForFingerprint) {
|
|
return Stage.TYPE_FINGERPRINT;
|
|
} else if (mForFace) {
|
|
return Stage.TYPE_FACE;
|
|
} else if (mForBiometrics) {
|
|
return Stage.TYPE_BIOMETRIC;
|
|
} else {
|
|
return Stage.TYPE_NONE;
|
|
}
|
|
}
|
|
|
|
private void setupPasswordRequirementsView(View view) {
|
|
mPasswordRestrictionView = view.findViewById(R.id.password_requirements_view);
|
|
mPasswordRestrictionView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
|
mPasswordRequirementAdapter = new PasswordRequirementAdapter();
|
|
mPasswordRestrictionView.setAdapter(mPasswordRequirementAdapter);
|
|
}
|
|
|
|
@Override
|
|
public int getMetricsCategory() {
|
|
return SettingsEnums.CHOOSE_LOCK_PASSWORD;
|
|
}
|
|
|
|
@Override
|
|
public void onResume() {
|
|
super.onResume();
|
|
updateStage(mUiStage);
|
|
if (mSaveAndFinishWorker != null) {
|
|
mSaveAndFinishWorker.setListener(this);
|
|
} else {
|
|
mPasswordEntry.requestFocus();
|
|
mPasswordEntry.scheduleShowSoftInput();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onPause() {
|
|
if (mSaveAndFinishWorker != null) {
|
|
mSaveAndFinishWorker.setListener(null);
|
|
}
|
|
super.onPause();
|
|
}
|
|
|
|
@Override
|
|
public void onSaveInstanceState(Bundle outState) {
|
|
super.onSaveInstanceState(outState);
|
|
outState.putString(KEY_UI_STAGE, mUiStage.name());
|
|
outState.putParcelable(KEY_FIRST_PASSWORD, mFirstPassword);
|
|
outState.putParcelable(KEY_CURRENT_CREDENTIAL, mCurrentCredential);
|
|
}
|
|
|
|
@Override
|
|
public void onActivityResult(int requestCode, int resultCode,
|
|
Intent data) {
|
|
super.onActivityResult(requestCode, resultCode, data);
|
|
switch (requestCode) {
|
|
case CONFIRM_EXISTING_REQUEST:
|
|
if (resultCode != Activity.RESULT_OK) {
|
|
getActivity().setResult(RESULT_FINISHED);
|
|
getActivity().finish();
|
|
} else {
|
|
mCurrentCredential = data.getParcelableExtra(
|
|
ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
protected Intent getRedactionInterstitialIntent(Context context) {
|
|
return RedactionInterstitial.createStartIntent(context, mUserId);
|
|
}
|
|
|
|
protected void updateStage(Stage stage) {
|
|
final Stage previousStage = mUiStage;
|
|
mUiStage = stage;
|
|
updateUi();
|
|
|
|
// If the stage changed, announce the header for accessibility. This
|
|
// is a no-op when accessibility is disabled.
|
|
if (previousStage != stage) {
|
|
mLayout.announceForAccessibility(mLayout.getHeaderText());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates PIN/Password and returns the validation result and updates mValidationErrors
|
|
* and mPasswordReused to reflect validation results.
|
|
*
|
|
* @param credential credential the user typed in.
|
|
* @return whether password satisfies all the requirements.
|
|
*/
|
|
@VisibleForTesting
|
|
boolean validatePassword(LockscreenCredential credential) {
|
|
final byte[] password = credential.getCredential();
|
|
mValidationErrors = PasswordMetrics.validatePassword(
|
|
mMinMetrics, mMinComplexity, !mIsAlphaMode, password);
|
|
if (mValidationErrors.isEmpty() && mLockPatternUtils.checkPasswordHistory(
|
|
password, getPasswordHistoryHashFactor(), mUserId)) {
|
|
mValidationErrors =
|
|
Collections.singletonList(new PasswordValidationError(RECENTLY_USED));
|
|
}
|
|
return mValidationErrors.isEmpty();
|
|
}
|
|
|
|
/**
|
|
* Lazily compute and return the history hash factor of the current user (mUserId), used for
|
|
* password history check.
|
|
*/
|
|
private byte[] getPasswordHistoryHashFactor() {
|
|
if (mPasswordHistoryHashFactor == null) {
|
|
mPasswordHistoryHashFactor = mLockPatternUtils.getPasswordHistoryHashFactor(
|
|
mCurrentCredential != null ? mCurrentCredential
|
|
: LockscreenCredential.createNone(), mUserId);
|
|
}
|
|
return mPasswordHistoryHashFactor;
|
|
}
|
|
|
|
public void handleNext() {
|
|
if (mSaveAndFinishWorker != null) return;
|
|
// TODO(b/120484642): This is a point of entry for passwords from the UI
|
|
final Editable passwordText = mPasswordEntry.getText();
|
|
if (TextUtils.isEmpty(passwordText)) {
|
|
return;
|
|
}
|
|
mChosenPassword = mIsAlphaMode ? LockscreenCredential.createPassword(passwordText)
|
|
: LockscreenCredential.createPin(passwordText);
|
|
if (mUiStage == Stage.Introduction) {
|
|
if (validatePassword(mChosenPassword)) {
|
|
mFirstPassword = mChosenPassword;
|
|
mPasswordEntry.setText("");
|
|
updateStage(Stage.NeedToConfirm);
|
|
} else {
|
|
mChosenPassword.zeroize();
|
|
}
|
|
} else if (mUiStage == Stage.NeedToConfirm) {
|
|
if (mChosenPassword.equals(mFirstPassword)) {
|
|
startSaveAndFinish();
|
|
} else {
|
|
CharSequence tmp = mPasswordEntry.getText();
|
|
if (tmp != null) {
|
|
Selection.setSelection((Spannable) tmp, 0, tmp.length());
|
|
}
|
|
updateStage(Stage.ConfirmWrong);
|
|
mChosenPassword.zeroize();
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void setNextEnabled(boolean enabled) {
|
|
mNextButton.setEnabled(enabled);
|
|
}
|
|
|
|
protected void setNextText(int text) {
|
|
mNextButton.setText(getActivity(), text);
|
|
}
|
|
|
|
protected void onSkipOrClearButtonClick(View view) {
|
|
mPasswordEntry.setText("");
|
|
}
|
|
|
|
protected void onNextButtonClick(View view) {
|
|
handleNext();
|
|
}
|
|
|
|
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
|
// Check if this was the result of hitting the enter or "done" key
|
|
if (actionId == EditorInfo.IME_NULL
|
|
|| actionId == EditorInfo.IME_ACTION_DONE
|
|
|| actionId == EditorInfo.IME_ACTION_NEXT) {
|
|
handleNext();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param errorCode error code returned from password validation.
|
|
* @return an array of messages describing the error, important messages come first.
|
|
*/
|
|
String[] convertErrorCodeToMessages() {
|
|
List<String> messages = new ArrayList<>();
|
|
for (PasswordValidationError error : mValidationErrors) {
|
|
switch (error.errorCode) {
|
|
case CONTAINS_INVALID_CHARACTERS:
|
|
messages.add(getString(R.string.lockpassword_illegal_character));
|
|
break;
|
|
case NOT_ENOUGH_UPPER_CASE:
|
|
messages.add(getResources().getQuantityString(
|
|
R.plurals.lockpassword_password_requires_uppercase,
|
|
error.requirement, error.requirement));
|
|
break;
|
|
case NOT_ENOUGH_LOWER_CASE:
|
|
messages.add(getResources().getQuantityString(
|
|
R.plurals.lockpassword_password_requires_lowercase,
|
|
error.requirement, error.requirement));
|
|
break;
|
|
case NOT_ENOUGH_LETTERS:
|
|
messages.add(getResources().getQuantityString(
|
|
R.plurals.lockpassword_password_requires_letters,
|
|
error.requirement, error.requirement));
|
|
break;
|
|
case NOT_ENOUGH_DIGITS:
|
|
messages.add(getResources().getQuantityString(
|
|
R.plurals.lockpassword_password_requires_numeric,
|
|
error.requirement, error.requirement));
|
|
break;
|
|
case NOT_ENOUGH_SYMBOLS:
|
|
messages.add(getResources().getQuantityString(
|
|
R.plurals.lockpassword_password_requires_symbols,
|
|
error.requirement, error.requirement));
|
|
break;
|
|
case NOT_ENOUGH_NON_LETTER:
|
|
messages.add(getResources().getQuantityString(
|
|
R.plurals.lockpassword_password_requires_nonletter,
|
|
error.requirement, error.requirement));
|
|
break;
|
|
case NOT_ENOUGH_NON_DIGITS:
|
|
messages.add(getResources().getQuantityString(
|
|
R.plurals.lockpassword_password_requires_nonnumerical,
|
|
error.requirement, error.requirement));
|
|
break;
|
|
case TOO_SHORT:
|
|
messages.add(getResources().getQuantityString(
|
|
mIsAlphaMode
|
|
? R.plurals.lockpassword_password_too_short
|
|
: R.plurals.lockpassword_pin_too_short,
|
|
error.requirement, error.requirement));
|
|
break;
|
|
case TOO_LONG:
|
|
messages.add(getResources().getQuantityString(
|
|
mIsAlphaMode
|
|
? R.plurals.lockpassword_password_too_long
|
|
: R.plurals.lockpassword_pin_too_long,
|
|
error.requirement + 1, error.requirement + 1));
|
|
break;
|
|
case CONTAINS_SEQUENCE:
|
|
messages.add(getString(R.string.lockpassword_pin_no_sequential_digits));
|
|
break;
|
|
case RECENTLY_USED:
|
|
messages.add(getString(mIsAlphaMode
|
|
? R.string.lockpassword_password_recently_used
|
|
: R.string.lockpassword_pin_recently_used));
|
|
break;
|
|
default:
|
|
Log.wtf(TAG, "unknown error validating password: " + error);
|
|
}
|
|
}
|
|
|
|
return messages.toArray(new String[0]);
|
|
}
|
|
|
|
/**
|
|
* Update the hint based on current Stage and length of password entry
|
|
*/
|
|
protected void updateUi() {
|
|
final boolean canInput = mSaveAndFinishWorker == null;
|
|
|
|
LockscreenCredential password = mIsAlphaMode
|
|
? LockscreenCredential.createPasswordOrNone(mPasswordEntry.getText())
|
|
: LockscreenCredential.createPinOrNone(mPasswordEntry.getText());
|
|
final int length = password.size();
|
|
if (mUiStage == Stage.Introduction) {
|
|
mPasswordRestrictionView.setVisibility(View.VISIBLE);
|
|
final boolean passwordCompliant = validatePassword(password);
|
|
String[] messages = convertErrorCodeToMessages();
|
|
// Update the fulfillment of requirements.
|
|
mPasswordRequirementAdapter.setRequirements(messages);
|
|
// Enable/Disable the next button accordingly.
|
|
setNextEnabled(passwordCompliant);
|
|
} else {
|
|
// Hide password requirement view when we are just asking user to confirm the pw.
|
|
mPasswordRestrictionView.setVisibility(View.GONE);
|
|
setHeaderText(getString(mUiStage.getHint(mIsAlphaMode, getStageType())));
|
|
setNextEnabled(canInput && length >= LockPatternUtils.MIN_LOCK_PASSWORD_SIZE);
|
|
mSkipOrClearButton.setVisibility(toVisibility(canInput && length > 0));
|
|
}
|
|
int message = mUiStage.getMessage(mIsAlphaMode, getStageType());
|
|
if (message != 0) {
|
|
mMessage.setVisibility(View.VISIBLE);
|
|
mMessage.setText(message);
|
|
} else {
|
|
mMessage.setVisibility(View.INVISIBLE);
|
|
}
|
|
|
|
setNextText(mUiStage.buttonText);
|
|
mPasswordEntryInputDisabler.setInputEnabled(canInput);
|
|
password.zeroize();
|
|
}
|
|
|
|
protected int toVisibility(boolean visibleOrGone) {
|
|
return visibleOrGone ? View.VISIBLE : View.GONE;
|
|
}
|
|
|
|
private void setHeaderText(String text) {
|
|
// Only set the text if it is different than the existing one to avoid announcing again.
|
|
if (!TextUtils.isEmpty(mLayout.getHeaderText())
|
|
&& mLayout.getHeaderText().toString().equals(text)) {
|
|
return;
|
|
}
|
|
mLayout.setHeaderText(text);
|
|
}
|
|
|
|
public void afterTextChanged(Editable s) {
|
|
// Changing the text while error displayed resets to NeedToConfirm state
|
|
if (mUiStage == Stage.ConfirmWrong) {
|
|
mUiStage = Stage.NeedToConfirm;
|
|
}
|
|
// Schedule the UI update.
|
|
mTextChangedHandler.notifyAfterTextChanged();
|
|
}
|
|
|
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
|
|
|
}
|
|
|
|
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
|
|
|
}
|
|
|
|
private void startSaveAndFinish() {
|
|
if (mSaveAndFinishWorker != null) {
|
|
Log.w(TAG, "startSaveAndFinish with an existing SaveAndFinishWorker.");
|
|
return;
|
|
}
|
|
|
|
mPasswordEntryInputDisabler.setInputEnabled(false);
|
|
setNextEnabled(false);
|
|
|
|
mSaveAndFinishWorker = new SaveAndFinishWorker();
|
|
mSaveAndFinishWorker.setListener(this);
|
|
|
|
getFragmentManager().beginTransaction().add(mSaveAndFinishWorker,
|
|
FRAGMENT_TAG_SAVE_AND_FINISH).commit();
|
|
getFragmentManager().executePendingTransactions();
|
|
|
|
final Intent intent = getActivity().getIntent();
|
|
final boolean required = intent.getBooleanExtra(
|
|
EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true);
|
|
if (mUnificationProfileId != UserHandle.USER_NULL) {
|
|
try (LockscreenCredential profileCredential = (LockscreenCredential)
|
|
intent.getParcelableExtra(EXTRA_KEY_UNIFICATION_PROFILE_CREDENTIAL)) {
|
|
mSaveAndFinishWorker.setProfileToUnify(mUnificationProfileId,
|
|
profileCredential);
|
|
}
|
|
}
|
|
mSaveAndFinishWorker.start(mLockPatternUtils, required, mRequestGatekeeperPassword,
|
|
mChosenPassword, mCurrentCredential, mUserId);
|
|
}
|
|
|
|
@Override
|
|
public void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData) {
|
|
getActivity().setResult(RESULT_FINISHED, resultData);
|
|
|
|
if (mChosenPassword != null) {
|
|
mChosenPassword.zeroize();
|
|
}
|
|
if (mCurrentCredential != null) {
|
|
mCurrentCredential.zeroize();
|
|
}
|
|
if (mFirstPassword != null) {
|
|
mFirstPassword.zeroize();
|
|
}
|
|
|
|
mPasswordEntry.setText("");
|
|
|
|
if (!wasSecureBefore) {
|
|
Intent intent = getRedactionInterstitialIntent(getActivity());
|
|
if (intent != null) {
|
|
startActivity(intent);
|
|
}
|
|
}
|
|
getActivity().finish();
|
|
}
|
|
|
|
class TextChangedHandler extends Handler {
|
|
private static final int ON_TEXT_CHANGED = 1;
|
|
private static final int DELAY_IN_MILLISECOND = 100;
|
|
|
|
/**
|
|
* With the introduction of delay, we batch processing the text changed event to reduce
|
|
* unnecessary UI updates.
|
|
*/
|
|
private void notifyAfterTextChanged() {
|
|
removeMessages(ON_TEXT_CHANGED);
|
|
sendEmptyMessageDelayed(ON_TEXT_CHANGED, DELAY_IN_MILLISECOND);
|
|
}
|
|
|
|
@Override
|
|
public void handleMessage(Message msg) {
|
|
if (getActivity() == null) {
|
|
return;
|
|
}
|
|
if (msg.what == ON_TEXT_CHANGED) {
|
|
updateUi();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static class SaveAndFinishWorker extends SaveChosenLockWorkerBase {
|
|
|
|
private LockscreenCredential mChosenPassword;
|
|
private LockscreenCredential mCurrentCredential;
|
|
|
|
public void start(LockPatternUtils utils, boolean required,
|
|
boolean requestGatekeeperPassword, LockscreenCredential chosenPassword,
|
|
LockscreenCredential currentCredential, int userId) {
|
|
prepare(utils, required, requestGatekeeperPassword, userId);
|
|
|
|
mChosenPassword = chosenPassword;
|
|
mCurrentCredential = currentCredential != null ? currentCredential
|
|
: LockscreenCredential.createNone();
|
|
mUserId = userId;
|
|
|
|
start();
|
|
}
|
|
|
|
@Override
|
|
protected Pair<Boolean, Intent> saveAndVerifyInBackground() {
|
|
final boolean success = mUtils.setLockCredential(
|
|
mChosenPassword, mCurrentCredential, mUserId);
|
|
if (success) {
|
|
unifyProfileCredentialIfRequested();
|
|
}
|
|
Intent result = null;
|
|
if (success && mRequestGatekeeperPassword) {
|
|
// If a Gatekeeper Password was requested, invoke the LockSettingsService code
|
|
// path to return a Gatekeeper Password based on the credential that the user
|
|
// chose. This should only be run if the credential was successfully set.
|
|
final VerifyCredentialResponse response = mUtils.verifyCredential(mChosenPassword,
|
|
mUserId, LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE);
|
|
|
|
if (!response.isMatched() || !response.containsGatekeeperPasswordHandle()) {
|
|
Log.e(TAG, "critical: bad response or missing GK PW handle for known good"
|
|
+ " password: " + response.toString());
|
|
}
|
|
|
|
result = new Intent();
|
|
result.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE,
|
|
response.getGatekeeperPasswordHandle());
|
|
}
|
|
return Pair.create(success, result);
|
|
}
|
|
}
|
|
}
|