blob: 793ea58f95501c457c95a8b8056a43e5aa3378e5 [file] [log] [blame]
/*
* 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.car.settings.security;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.UserHandle;
import android.text.Editable;
import android.text.Selection;
import android.text.Spannable;
import android.text.TextWatcher;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.DrawableRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;
import com.android.car.settings.R;
import com.android.car.settings.common.BaseFragment;
import com.android.car.settings.common.Logger;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.TextViewInputDisabler;
import java.util.Arrays;
import java.util.List;
/**
* Fragment for choosing a lock password/pin.
*/
public class ChooseLockPinPasswordFragment extends BaseFragment {
private static final String LOCK_OPTIONS_DIALOG_TAG = "lock_options_dialog_tag";
private static final String FRAGMENT_TAG_SAVE_PASSWORD_WORKER = "save_password_worker";
private static final String STATE_UI_STAGE = "state_ui_stage";
private static final String STATE_FIRST_ENTRY = "state_first_entry";
private static final Logger LOG = new Logger(ChooseLockPinPasswordFragment.class);
private static final String EXTRA_IS_PIN = "extra_is_pin";
private Stage mUiStage = Stage.Introduction;
private int mUserId;
private int mErrorCode = PasswordHelper.NO_ERROR;
private boolean mIsPin;
private boolean mIsAlphaMode;
// Password currently in the input field
private byte[] mCurrentEntry;
// Existing password that user previously set
private byte[] mExistingPassword;
// Password must be entered twice. This is what user entered the first time.
private byte[] mFirstEntry;
private PinPadView mPinPad;
private TextView mHintMessage;
private Button mSecondaryButton;
private Button mPrimaryButton;
private EditText mPasswordField;
private ProgressBar mProgressBar;
private TextChangedHandler mTextChangedHandler = new TextChangedHandler();
private TextViewInputDisabler mPasswordEntryInputDisabler;
private SavePasswordWorker mSavePasswordWorker;
private PasswordHelper mPasswordHelper;
/**
* Factory method for creating fragment in password mode
*/
public static ChooseLockPinPasswordFragment newPasswordInstance() {
ChooseLockPinPasswordFragment passwordFragment = new ChooseLockPinPasswordFragment();
Bundle bundle = new Bundle();
bundle.putBoolean(EXTRA_IS_PIN, false);
passwordFragment.setArguments(bundle);
return passwordFragment;
}
/**
* Factory method for creating fragment in Pin mode
*/
public static ChooseLockPinPasswordFragment newPinInstance() {
ChooseLockPinPasswordFragment passwordFragment = new ChooseLockPinPasswordFragment();
Bundle bundle = new Bundle();
bundle.putBoolean(EXTRA_IS_PIN, true);
passwordFragment.setArguments(bundle);
return passwordFragment;
}
@Override
@LayoutRes
protected int getActionBarLayoutId() {
return R.layout.action_bar_with_button;
}
@Override
@LayoutRes
protected int getLayoutId() {
return mIsPin ? R.layout.choose_lock_pin : R.layout.choose_lock_password;
}
@Override
@StringRes
protected int getTitleId() {
return mIsPin ? R.string.security_lock_pin : R.string.security_lock_password;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mUserId = UserHandle.myUserId();
Bundle args = getArguments();
if (args != null) {
mIsPin = args.getBoolean(EXTRA_IS_PIN);
mExistingPassword = args.getByteArray(PasswordHelper.EXTRA_CURRENT_SCREEN_LOCK);
}
mPasswordHelper = new PasswordHelper(mIsPin);
int passwordQuality = mPasswordHelper.getPasswordQuality();
mIsAlphaMode = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == passwordQuality
|| DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == passwordQuality
|| DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == passwordQuality;
if (savedInstanceState != null) {
mUiStage = Stage.values()[savedInstanceState.getInt(STATE_UI_STAGE)];
mFirstEntry = savedInstanceState.getByteArray(STATE_FIRST_ENTRY);
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mPasswordField = view.findViewById(R.id.password_entry);
mPasswordField.setOnEditorActionListener((textView, actionId, keyEvent) -> {
// 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) {
handlePrimaryButtonClick();
return true;
}
return false;
});
mPasswordField.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
// Changing the text while error displayed resets to a normal state
if (mUiStage == Stage.ConfirmWrong) {
mUiStage = Stage.NeedToConfirm;
} else if (mUiStage == Stage.PasswordInvalid) {
mUiStage = Stage.Introduction;
}
// Schedule the UI update.
mTextChangedHandler.notifyAfterTextChanged();
}
});
mPasswordEntryInputDisabler = new TextViewInputDisabler(mPasswordField);
mHintMessage = view.findViewById(R.id.hint_text);
if (mIsPin) {
initPinView(view);
} else {
mPasswordField.requestFocus();
InputMethodManager imm = (InputMethodManager)
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.showSoftInput(mPasswordField, InputMethodManager.SHOW_IMPLICIT);
}
}
// Re-attach to the exiting worker if there is one.
if (savedInstanceState != null) {
mSavePasswordWorker = (SavePasswordWorker) getFragmentManager().findFragmentByTag(
FRAGMENT_TAG_SAVE_PASSWORD_WORKER);
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mProgressBar = requireActivity().findViewById(R.id.progress_bar);
mPrimaryButton = requireActivity().findViewById(R.id.action_button1);
mPrimaryButton.setOnClickListener(view -> handlePrimaryButtonClick());
mSecondaryButton = getActivity().findViewById(R.id.action_button2);
mSecondaryButton.setVisibility(View.VISIBLE);
mSecondaryButton.setOnClickListener(view -> handleSecondaryButtonClick());
}
@Override
public void onStart() {
super.onStart();
updateStage(mUiStage);
if (mSavePasswordWorker != null) {
mSavePasswordWorker.setListener(this::onChosenLockSaveFinished);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(STATE_UI_STAGE, mUiStage.ordinal());
outState.putByteArray(STATE_FIRST_ENTRY, mFirstEntry);
}
@Override
public void onStop() {
super.onStop();
if (mSavePasswordWorker != null) {
mSavePasswordWorker.setListener(null);
}
mProgressBar.setVisibility(View.GONE);
}
/**
* Append the argument to the end of the password entry field
*/
private void appendToPasswordEntry(String text) {
mPasswordField.append(text);
}
/**
* Returns the string in the password entry field
*/
@Nullable
private byte[] getEnteredPassword() {
return LockPatternUtils.charSequenceToByteArray(mPasswordField.getText());
}
private void initPinView(View view) {
mPinPad = view.findViewById(R.id.pin_pad);
PinPadView.PinPadClickListener pinPadClickListener = new PinPadView.PinPadClickListener() {
@Override
public void onDigitKeyClick(String digit) {
appendToPasswordEntry(digit);
}
@Override
public void onBackspaceClick() {
byte[] pin = getEnteredPassword();
if (pin != null && pin.length > 0) {
mPasswordField.getText().delete(mPasswordField.getSelectionEnd() - 1,
mPasswordField.getSelectionEnd());
}
if (pin != null) {
Arrays.fill(pin, (byte) 0);
}
}
@Override
public void onEnterKeyClick() {
handlePrimaryButtonClick();
}
};
mPinPad.setPinPadClickListener(pinPadClickListener);
}
private boolean shouldEnableSubmit() {
return getEnteredPassword() != null
&& getEnteredPassword().length >= PasswordHelper.MIN_LENGTH
&& (mSavePasswordWorker == null || mSavePasswordWorker.isFinished());
}
private void updateSubmitButtonsState() {
boolean enabled = shouldEnableSubmit();
mPrimaryButton.setEnabled(enabled);
if (mIsPin) {
mPinPad.setEnterKeyEnabled(enabled);
}
}
private void setPrimaryButtonText(@StringRes int textId) {
mPrimaryButton.setText(textId);
}
private void setSecondaryButtonEnabled(boolean enabled) {
mSecondaryButton.setEnabled(enabled);
}
private void setSecondaryButtonText(@StringRes int textId) {
mSecondaryButton.setText(textId);
}
// Updates display message and proceed to next step according to the different text on
// the primary button.
private void handlePrimaryButtonClick() {
// Need to check this because it can be fired from the keyboard.
if (!shouldEnableSubmit()) {
return;
}
mCurrentEntry = getEnteredPassword();
switch (mUiStage) {
case Introduction:
mErrorCode = mPasswordHelper.validate(mCurrentEntry);
if (mErrorCode == PasswordHelper.NO_ERROR) {
mFirstEntry = mCurrentEntry;
mPasswordField.setText("");
updateStage(Stage.NeedToConfirm);
} else {
updateStage(Stage.PasswordInvalid);
Arrays.fill(mCurrentEntry, (byte) 0);
}
break;
case NeedToConfirm:
case SaveFailure:
// Password must be entered twice. mFirstEntry is the one the user entered
// the first time. mCurrentEntry is what's currently in the input field
if (Arrays.equals(mFirstEntry, mCurrentEntry)) {
startSaveAndFinish();
} else {
CharSequence tmp = mPasswordField.getText();
if (tmp != null) {
Selection.setSelection((Spannable) tmp, 0, tmp.length());
}
updateStage(Stage.ConfirmWrong);
Arrays.fill(mCurrentEntry, (byte) 0);
}
break;
default:
// Do nothing.
}
}
// Updates display message and proceed to next step according to the different text on
// the secondary button.
private void handleSecondaryButtonClick() {
if (mSavePasswordWorker != null) {
return;
}
if (mUiStage.secondaryButtonText == R.string.lockpassword_clear_label) {
mPasswordField.setText("");
mUiStage = Stage.Introduction;
setSecondaryButtonText(mUiStage.secondaryButtonText);
} else {
getFragmentController().goBack();
}
}
@VisibleForTesting
void onChosenLockSaveFinished(boolean isSaveSuccessful) {
mProgressBar.setVisibility(View.GONE);
if (isSaveSuccessful) {
onComplete();
} else {
updateStage(Stage.SaveFailure);
}
}
// Starts an async task to save the chosen password.
private void startSaveAndFinish() {
if (mSavePasswordWorker != null && !mSavePasswordWorker.isFinished()) {
LOG.v("startSaveAndFinish with a running SaveAndFinishWorker.");
return;
}
mPasswordEntryInputDisabler.setInputEnabled(false);
if (mSavePasswordWorker == null) {
mSavePasswordWorker = new SavePasswordWorker();
mSavePasswordWorker.setListener(this::onChosenLockSaveFinished);
getFragmentManager()
.beginTransaction()
.add(mSavePasswordWorker, FRAGMENT_TAG_SAVE_PASSWORD_WORKER)
.commitNow();
}
mSavePasswordWorker.start(mUserId, mCurrentEntry, mExistingPassword,
mPasswordHelper.getPasswordQuality());
mProgressBar.setVisibility(View.VISIBLE);
updateSubmitButtonsState();
}
// Updates the hint message, error, button text and state
private void updateUi() {
updateSubmitButtonsState();
boolean inputAllowed = mSavePasswordWorker == null || mSavePasswordWorker.isFinished();
if (mUiStage != Stage.Introduction) {
setSecondaryButtonEnabled(inputAllowed);
}
if (mIsPin) {
mPinPad.setEnterKeyIcon(mUiStage.enterKeyIcon);
}
switch (mUiStage) {
case Introduction:
case NeedToConfirm:
mPasswordField.setError(null);
mHintMessage.setText(getString(mUiStage.getHint(mIsAlphaMode)));
break;
case PasswordInvalid:
List<String> messages =
mPasswordHelper.convertErrorCodeToMessages(getContext(), mErrorCode);
setError(String.join(" ", messages));
break;
case ConfirmWrong:
case SaveFailure:
setError(getString(mUiStage.getHint(mIsAlphaMode)));
break;
default:
// Do nothing
}
setPrimaryButtonText(mUiStage.primaryButtonText);
setSecondaryButtonText(mUiStage.secondaryButtonText);
mPasswordEntryInputDisabler.setInputEnabled(inputAllowed);
}
/**
* To show error in password, it is set directly on TextInputEditText. PIN can't use
* TextInputEditText because PIN field is not focusable therefore error won't show. Instead
* the error is shown as a hint message.
*/
private void setError(String message) {
mHintMessage.setText(message);
}
@VisibleForTesting
void updateStage(Stage stage) {
mUiStage = stage;
updateUi();
}
@VisibleForTesting
void onComplete() {
if (mCurrentEntry != null) {
Arrays.fill(mCurrentEntry, (byte) 0);
}
if (mExistingPassword != null) {
Arrays.fill(mExistingPassword, (byte) 0);
}
if (mFirstEntry != null) {
Arrays.fill(mFirstEntry, (byte) 0);
}
mPasswordField.setText("");
getActivity().finish();
}
// Keep track internally of where the user is in choosing a password.
@VisibleForTesting
enum Stage {
Introduction(
R.string.choose_lock_password_hints,
R.string.choose_lock_pin_hints,
R.string.continue_button_text,
R.string.lockpassword_cancel_label,
R.drawable.ic_arrow_forward),
PasswordInvalid(
R.string.lockpassword_invalid_password,
R.string.lockpin_invalid_pin,
R.string.continue_button_text,
R.string.lockpassword_clear_label,
R.drawable.ic_arrow_forward),
NeedToConfirm(
R.string.confirm_your_password_header,
R.string.confirm_your_pin_header,
R.string.lockpassword_confirm_label,
R.string.lockpassword_cancel_label,
R.drawable.ic_check),
ConfirmWrong(
R.string.confirm_passwords_dont_match,
R.string.confirm_pins_dont_match,
R.string.continue_button_text,
R.string.lockpassword_cancel_label,
R.drawable.ic_check),
SaveFailure(
R.string.error_saving_password,
R.string.error_saving_lockpin,
R.string.lockscreen_retry_button_text,
R.string.lockpassword_cancel_label,
R.drawable.ic_check);
public final int alphaHint;
public final int numericHint;
public final int primaryButtonText;
public final int secondaryButtonText;
public final int enterKeyIcon;
Stage(@StringRes int hintInAlpha,
@StringRes int hintInNumeric,
@StringRes int primaryButtonText,
@StringRes int secondaryButtonText,
@DrawableRes int enterKeyIcon) {
this.alphaHint = hintInAlpha;
this.numericHint = hintInNumeric;
this.primaryButtonText = primaryButtonText;
this.secondaryButtonText = secondaryButtonText;
this.enterKeyIcon = enterKeyIcon;
}
@StringRes
public int getHint(boolean isAlpha) {
if (isAlpha) {
return alphaHint;
} else {
return numericHint;
}
}
}
/**
* Handler that batches text changed events
*/
private 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 (msg.what == ON_TEXT_CHANGED) {
mErrorCode = PasswordHelper.NO_ERROR;
updateUi();
}
}
}
}