| /* |
| * 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.Activity; |
| 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.EditText; |
| import android.widget.TextView; |
| |
| import androidx.annotation.DrawableRes; |
| import androidx.annotation.LayoutRes; |
| import androidx.annotation.NonNull; |
| 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.car.ui.toolbar.MenuItem; |
| import com.android.car.ui.toolbar.ProgressBarController; |
| import com.android.internal.widget.LockscreenCredential; |
| import com.android.internal.widget.TextViewInputDisabler; |
| |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Objects; |
| |
| /** |
| * 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 boolean mIsPin; |
| |
| // Password currently in the input field |
| private LockscreenCredential mCurrentEntry; |
| // Existing password that user previously set |
| private LockscreenCredential mExistingCredential; |
| // Password must be entered twice. This is what user entered the first time. |
| private LockscreenCredential mFirstEntry; |
| |
| private PinPadView mPinPad; |
| private TextView mHintMessage; |
| private MenuItem mPrimaryButton; |
| private EditText mPasswordField; |
| private ProgressBarController mProgressBar; |
| |
| private TextChangedHandler mTextChangedHandler = new TextChangedHandler(); |
| private TextViewInputDisabler mPasswordEntryInputDisabler; |
| private SaveLockWorker mSaveLockWorker; |
| 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 |
| public List<MenuItem> getToolbarMenuItems() { |
| return Arrays.asList(mPrimaryButton); |
| } |
| |
| @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); |
| mExistingCredential = args.getParcelable(PasswordHelper.EXTRA_CURRENT_SCREEN_LOCK); |
| if (mExistingCredential != null) { |
| mExistingCredential = mExistingCredential.duplicate(); |
| } |
| } |
| |
| mPasswordHelper = new PasswordHelper(getContext(), mUserId); |
| |
| if (savedInstanceState != null) { |
| mUiStage = Stage.values()[savedInstanceState.getInt(STATE_UI_STAGE)]; |
| mFirstEntry = savedInstanceState.getParcelable(STATE_FIRST_ENTRY); |
| } |
| |
| mPrimaryButton = new MenuItem.Builder(getContext()) |
| .setOnClickListener(i -> handlePrimaryButtonClick()) |
| .build(); |
| } |
| |
| @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. |
| if (isResumed()) { |
| 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) { |
| mSaveLockWorker = (SaveLockWorker) getFragmentManager().findFragmentByTag( |
| FRAGMENT_TAG_SAVE_PASSWORD_WORKER); |
| } |
| } |
| |
| @Override |
| public void onActivityCreated(Bundle savedInstanceState) { |
| super.onActivityCreated(savedInstanceState); |
| mProgressBar = getToolbar().getProgressBar(); |
| } |
| |
| @Override |
| public void onStart() { |
| super.onStart(); |
| updateStage(mUiStage); |
| |
| if (mSaveLockWorker != null) { |
| mSaveLockWorker.setListener(this::onChosenLockSaveFinished); |
| } |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle outState) { |
| super.onSaveInstanceState(outState); |
| outState.putInt(STATE_UI_STAGE, mUiStage.ordinal()); |
| outState.putParcelable(STATE_FIRST_ENTRY, mFirstEntry); |
| } |
| |
| @Override |
| public void onStop() { |
| super.onStop(); |
| if (mSaveLockWorker != null) { |
| mSaveLockWorker.setListener(null); |
| } |
| mProgressBar.setVisible(false); |
| } |
| |
| @Override |
| public void onDestroy() { |
| super.onDestroy(); |
| mPasswordField.setText(null); |
| |
| PasswordHelper.zeroizeCredentials(mCurrentEntry, mExistingCredential, mFirstEntry); |
| } |
| |
| /** |
| * 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 |
| */ |
| @NonNull |
| private LockscreenCredential getEnteredPassword() { |
| if (mIsPin) { |
| return LockscreenCredential.createPinOrNone(mPasswordField.getText()); |
| } else { |
| return LockscreenCredential.createPasswordOrNone(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() { |
| try (LockscreenCredential pin = getEnteredPassword()) { |
| if (pin.size() > 0) { |
| mPasswordField.getText().delete(mPasswordField.getSelectionEnd() - 1, |
| mPasswordField.getSelectionEnd()); |
| } |
| } |
| } |
| |
| @Override |
| public void onEnterKeyClick() { |
| handlePrimaryButtonClick(); |
| } |
| }; |
| |
| mPinPad.setPinPadClickListener(pinPadClickListener); |
| } |
| |
| private boolean shouldEnableSubmit() { |
| try (LockscreenCredential enteredCredential = getEnteredPassword()) { |
| return mPasswordHelper.validateCredential(enteredCredential, mExistingCredential) |
| && (mSaveLockWorker == null || mSaveLockWorker.isFinished()); |
| } |
| } |
| |
| private void updateSubmitButtonsState() { |
| boolean enabled = shouldEnableSubmit(); |
| |
| mPrimaryButton.setEnabled(enabled); |
| if (mIsPin) { |
| mPinPad.setEnterKeyEnabled(enabled); |
| } |
| } |
| |
| private void setPrimaryButtonText(@StringRes int textId) { |
| mPrimaryButton.setTitle(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: |
| boolean passwordCompliant = |
| mPasswordHelper.validateCredential(mCurrentEntry, mExistingCredential); |
| if (passwordCompliant) { |
| mFirstEntry = mCurrentEntry; |
| mPasswordField.setText(""); |
| updateStage(Stage.NeedToConfirm); |
| } else { |
| updateStage(Stage.PasswordInvalid); |
| mCurrentEntry.zeroize(); |
| } |
| 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 (Objects.equals(mFirstEntry, mCurrentEntry)) { |
| startSaveAndFinish(); |
| } else { |
| CharSequence tmp = mPasswordField.getText(); |
| if (tmp != null) { |
| Selection.setSelection((Spannable) tmp, 0, tmp.length()); |
| } |
| updateStage(Stage.ConfirmWrong); |
| mCurrentEntry.zeroize(); |
| } |
| break; |
| default: |
| // Do nothing. |
| } |
| } |
| |
| @VisibleForTesting |
| void onChosenLockSaveFinished(boolean isSaveSuccessful) { |
| mProgressBar.setVisible(false); |
| if (isSaveSuccessful) { |
| onComplete(); |
| } else { |
| updateStage(Stage.SaveFailure); |
| } |
| } |
| |
| // Starts an async task to save the chosen password. |
| private void startSaveAndFinish() { |
| if (mSaveLockWorker != null && !mSaveLockWorker.isFinished()) { |
| LOG.v("startSaveAndFinish with a running SaveAndFinishWorker."); |
| return; |
| } |
| |
| mPasswordEntryInputDisabler.setInputEnabled(false); |
| |
| if (mSaveLockWorker == null) { |
| mSaveLockWorker = new SaveLockWorker(); |
| mSaveLockWorker.setListener(this::onChosenLockSaveFinished); |
| |
| getFragmentManager() |
| .beginTransaction() |
| .add(mSaveLockWorker, FRAGMENT_TAG_SAVE_PASSWORD_WORKER) |
| .commitNow(); |
| } |
| |
| mSaveLockWorker.start(mUserId, mCurrentEntry, mExistingCredential); |
| |
| mProgressBar.setVisible(true); |
| updateSubmitButtonsState(); |
| } |
| |
| // Updates the hint message, error, button text and state |
| private void updateUi() { |
| updateSubmitButtonsState(); |
| |
| boolean inputAllowed = mSaveLockWorker == null || mSaveLockWorker.isFinished(); |
| |
| if (mIsPin) { |
| mPinPad.setEnterKeyIcon(mUiStage.enterKeyIcon); |
| } |
| |
| try (LockscreenCredential enteredCredential = getEnteredPassword()) { |
| mPasswordHelper.validateCredential(enteredCredential, mExistingCredential); |
| } |
| mHintMessage.setText(mPasswordHelper.getCredentialValidationErrorMessages()); |
| |
| setHintIfNeeded(); |
| setPrimaryButtonText(mUiStage.primaryButtonText); |
| mPasswordEntryInputDisabler.setInputEnabled(inputAllowed); |
| } |
| |
| private void setHintIfNeeded() { |
| if (!mHintMessage.getText().toString().isEmpty()) { |
| return; |
| } |
| |
| if (mUiStage == Stage.ConfirmWrong) { |
| mHintMessage.setText(mIsPin ? R.string.confirm_pins_dont_match |
| : R.string.confirm_passwords_dont_match); |
| } else if (mUiStage == Stage.SaveFailure) { |
| mHintMessage.setText(mIsPin ? R.string.error_saving_lockpin |
| : R.string.error_saving_password); |
| } |
| } |
| |
| @VisibleForTesting |
| void updateStage(Stage stage) { |
| mUiStage = stage; |
| updateUi(); |
| } |
| |
| @VisibleForTesting |
| void onComplete() { |
| if (mCurrentEntry != null) { |
| mCurrentEntry.zeroize(); |
| } |
| |
| if (mExistingCredential != null) { |
| mExistingCredential.zeroize(); |
| } |
| |
| if (mFirstEntry != null) { |
| mFirstEntry.zeroize(); |
| } |
| |
| mPasswordField.setText(""); |
| |
| getActivity().setResult(Activity.RESULT_OK); |
| getActivity().finish(); |
| } |
| |
| @VisibleForTesting |
| void setPasswordHelper(PasswordHelper passwordHelper) { |
| mPasswordHelper = passwordHelper; |
| } |
| |
| @VisibleForTesting |
| String getHintText() { |
| return mHintMessage.getText().toString(); |
| } |
| |
| // Keep track internally of where the user is in choosing a password. |
| @VisibleForTesting |
| enum Stage { |
| Introduction( |
| R.string.continue_button_text, |
| R.drawable.ic_arrow_forward), |
| |
| PasswordInvalid( |
| R.string.continue_button_text, |
| R.drawable.ic_arrow_forward), |
| |
| NeedToConfirm( |
| R.string.lockpassword_confirm_label, |
| R.drawable.ic_check), |
| |
| ConfirmWrong( |
| R.string.lockpassword_confirm_label, |
| R.drawable.ic_check), |
| |
| SaveFailure( |
| R.string.lockscreen_retry_button_text, |
| R.drawable.ic_check); |
| |
| public final int primaryButtonText; |
| public final int enterKeyIcon; |
| |
| Stage(@StringRes int primaryButtonText, |
| @DrawableRes int enterKeyIcon) { |
| this.primaryButtonText = primaryButtonText; |
| this.enterKeyIcon = enterKeyIcon; |
| } |
| } |
| |
| /** |
| * 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) { |
| updateUi(); |
| } |
| } |
| } |
| } |