/* * 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.os.Bundle; import android.os.UserHandle; import android.view.View; import android.widget.TextView; import androidx.annotation.LayoutRes; 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.internal.widget.LockPatternUtils; import com.android.internal.widget.LockPatternView; import com.android.internal.widget.LockPatternView.Cell; import com.android.internal.widget.LockPatternView.DisplayMode; import com.android.internal.widget.LockscreenCredential; import com.google.android.collect.Lists; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * Fragment for choosing security lock pattern. */ public class ChooseLockPatternFragment extends BaseFragment { private static final Logger LOG = new Logger(ChooseLockPatternFragment.class); private static final String FRAGMENT_TAG_SAVE_PATTERN_WORKER = "save_pattern_worker"; private static final String STATE_UI_STAGE = "state_ui_stage"; private static final String STATE_CHOSEN_PATTERN = "state_chosen_pattern"; private static final int ID_EMPTY_MESSAGE = -1; /** * The patten used during the help screen to show how to draw a pattern. */ private final List mAnimatePattern = Collections.unmodifiableList(Lists.newArrayList( LockPatternView.Cell.of(0, 0), LockPatternView.Cell.of(0, 1), LockPatternView.Cell.of(1, 1), LockPatternView.Cell.of(2, 1) )); // How long we wait to clear a wrong pattern private int mWrongPatternClearTimeOut; private int mUserId; private Stage mUiStage = Stage.Introduction; private LockPatternView mLockPatternView; private TextView mMessageText; private LockscreenCredential mChosenPattern; private MenuItem mSecondaryButton; private MenuItem mPrimaryButton; // Existing pattern that user previously set private LockscreenCredential mCurrentCredential; private SaveLockWorker mSaveLockWorker; private Runnable mClearPatternRunnable = () -> mLockPatternView.clearPattern(); // The pattern listener that responds according to a user choosing a new // lock pattern. private final LockPatternView.OnPatternListener mChooseNewLockPatternListener = new LockPatternView.OnPatternListener() { @Override public void onPatternStart() { mLockPatternView.removeCallbacks(mClearPatternRunnable); updateUIWhenPatternInProgress(); } @Override public void onPatternCleared() { mLockPatternView.removeCallbacks(mClearPatternRunnable); } @Override public void onPatternDetected(List pattern) { switch (mUiStage) { case Introduction: case ChoiceTooShort: handlePatternEntered(pattern); break; case ConfirmWrong: case NeedToConfirm: handleConfirmPattern(pattern); break; default: throw new IllegalStateException("Unexpected stage " + mUiStage + " when entering the pattern."); } } @Override public void onPatternCellAdded(List pattern) { } private void handleConfirmPattern(List pattern) { if (mChosenPattern == null) { throw new IllegalStateException( "null chosen pattern in stage 'need to confirm"); } try (LockscreenCredential credential = LockscreenCredential.createPattern(pattern)) { if (mChosenPattern.equals(credential)) { updateStage(Stage.ChoiceConfirmed); } else { updateStage(Stage.ConfirmWrong); } } } private void handlePatternEntered(List pattern) { if (pattern.size() < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) { updateStage(Stage.ChoiceTooShort); } else { mChosenPattern = LockscreenCredential.createPattern(pattern); updateStage(Stage.FirstChoiceValid); } } }; /** * Factory method for creating ChooseLockPatternFragment */ public static ChooseLockPatternFragment newInstance() { ChooseLockPatternFragment patternFragment = new ChooseLockPatternFragment(); return patternFragment; } @Override public List getToolbarMenuItems() { return Arrays.asList(mSecondaryButton, mPrimaryButton); } @Override @LayoutRes protected int getLayoutId() { return R.layout.choose_lock_pattern; } @Override @StringRes protected int getTitleId() { return R.string.security_lock_pattern; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mWrongPatternClearTimeOut = getResources().getInteger(R.integer.clear_content_timeout_ms); mUserId = UserHandle.myUserId(); Bundle args = getArguments(); if (args != null) { mCurrentCredential = args.getParcelable(PasswordHelper.EXTRA_CURRENT_SCREEN_LOCK); if (mCurrentCredential != null) { mCurrentCredential = mCurrentCredential.duplicate(); } } if (savedInstanceState != null) { mUiStage = Stage.values()[savedInstanceState.getInt(STATE_UI_STAGE)]; mChosenPattern = savedInstanceState.getParcelable(STATE_CHOSEN_PATTERN); } mPrimaryButton = new MenuItem.Builder(getContext()) .setOnClickListener(i -> handlePrimaryButtonClick()) .build(); mSecondaryButton = new MenuItem.Builder(getContext()) .setOnClickListener(i -> handleSecondaryButtonClick()) .build(); } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mMessageText = view.findViewById(R.id.title_text); mMessageText.setText(getString(R.string.choose_lock_pattern_message)); mLockPatternView = view.findViewById(R.id.lockPattern); mLockPatternView.setVisibility(View.VISIBLE); mLockPatternView.setEnabled(true); mLockPatternView.setFadePattern(false); mLockPatternView.clearPattern(); mLockPatternView.setOnPatternListener(mChooseNewLockPatternListener); // Re-attach to the exiting worker if there is one. if (savedInstanceState != null) { mSaveLockWorker = (SaveLockWorker) getFragmentManager().findFragmentByTag( FRAGMENT_TAG_SAVE_PATTERN_WORKER); } } @Override public void onStart() { super.onStart(); updateStage(mUiStage); if (mSaveLockWorker != null) { setPrimaryButtonEnabled(true); mSaveLockWorker.setListener(this::onChosenLockSaveFinished); } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt(STATE_UI_STAGE, mUiStage.ordinal()); outState.putParcelable(STATE_CHOSEN_PATTERN, mChosenPattern); } @Override public void onStop() { super.onStop(); if (mSaveLockWorker != null) { mSaveLockWorker.setListener(null); } getToolbar().getProgressBar().setVisible(false); } @Override public void onDestroy() { super.onDestroy(); mLockPatternView.clearPattern(); PasswordHelper.zeroizeCredentials(mChosenPattern, mCurrentCredential); } /** * Updates the messages and buttons appropriate to what stage the user * is at in choosing a pattern. This doesn't handle clearing out the pattern; * the pattern is expected to be in the right state. * * @param stage The stage UI should be updated to match with. */ protected void updateStage(Stage stage) { mUiStage = stage; // Message mText, visibility and // mEnabled state all known from the stage mMessageText.setText(stage.mMessageId); if (stage.mSecondaryButtonState == SecondaryButtonState.Gone) { setSecondaryButtonVisible(false); } else { setSecondaryButtonVisible(true); setSecondaryButtonText(stage.mSecondaryButtonState.mTextResId); setSecondaryButtonEnabled(stage.mSecondaryButtonState.mEnabled); } setPrimaryButtonText(stage.mPrimaryButtonState.mText); setPrimaryButtonEnabled(stage.mPrimaryButtonState.mEnabled); // same for whether the pattern is mEnabled if (stage.mPatternEnabled) { mLockPatternView.enableInput(); } else { mLockPatternView.disableInput(); } // the rest of the stuff varies enough that it is easier just to handle // on a case by case basis. mLockPatternView.setDisplayMode(DisplayMode.Correct); switch (mUiStage) { case Introduction: mLockPatternView.clearPattern(); break; case HelpScreen: mLockPatternView.setPattern(DisplayMode.Animate, mAnimatePattern); break; case ChoiceTooShort: mLockPatternView.setDisplayMode(DisplayMode.Wrong); postClearPatternRunnable(); break; case FirstChoiceValid: break; case NeedToConfirm: mLockPatternView.clearPattern(); break; case ConfirmWrong: mLockPatternView.setDisplayMode(DisplayMode.Wrong); postClearPatternRunnable(); break; case ChoiceConfirmed: break; default: // Do nothing. } } private void updateUIWhenPatternInProgress() { mMessageText.setText(R.string.lockpattern_recording_inprogress); setPrimaryButtonEnabled(false); setSecondaryButtonEnabled(false); } // clear the wrong pattern unless they have started a new one // already private void postClearPatternRunnable() { mLockPatternView.removeCallbacks(mClearPatternRunnable); mLockPatternView.postDelayed(mClearPatternRunnable, mWrongPatternClearTimeOut); } private void setPrimaryButtonEnabled(boolean enabled) { mPrimaryButton.setEnabled(enabled); } private void setPrimaryButtonText(@StringRes int textId) { mPrimaryButton.setTitle(textId); } private void setSecondaryButtonVisible(boolean visible) { mSecondaryButton.setVisible(visible); } private void setSecondaryButtonEnabled(boolean enabled) { mSecondaryButton.setEnabled(enabled); } private void setSecondaryButtonText(@StringRes int textId) { mSecondaryButton.setTitle(textId); } // Update display message and decide on next step according to the different mText // on the primary button private void handlePrimaryButtonClick() { switch (mUiStage.mPrimaryButtonState) { case Continue: if (mUiStage != Stage.FirstChoiceValid) { throw new IllegalStateException("expected ui stage " + Stage.FirstChoiceValid + " when button is " + PrimaryButtonState.Continue); } updateStage(Stage.NeedToConfirm); break; case Confirm: if (mUiStage != Stage.ChoiceConfirmed) { throw new IllegalStateException("expected ui stage " + Stage.ChoiceConfirmed + " when button is " + PrimaryButtonState.Confirm); } startSaveAndFinish(); break; case Retry: if (mUiStage != Stage.SaveFailure) { throw new IllegalStateException("expected ui stage " + Stage.SaveFailure + " when button is " + PrimaryButtonState.Retry); } startSaveAndFinish(); break; case Ok: if (mUiStage != Stage.HelpScreen) { throw new IllegalStateException("Help screen is only mode with ok button, " + "but stage is " + mUiStage); } mLockPatternView.clearPattern(); mLockPatternView.setDisplayMode(DisplayMode.Correct); updateStage(Stage.Introduction); break; default: // Do nothing. } } // Update display message and proceed to next step according to the different mText on // the secondary button. private void handleSecondaryButtonClick() { if (mUiStage.mSecondaryButtonState == SecondaryButtonState.Retry) { mChosenPattern = null; mLockPatternView.clearPattern(); updateStage(Stage.Introduction); } else { throw new IllegalStateException("secondary button pressed, but stage of " + mUiStage + " doesn't make sense"); } } @VisibleForTesting void onChosenLockSaveFinished(boolean isSaveSuccessful) { getToolbar().getProgressBar().setVisible(false); if (isSaveSuccessful) { onComplete(); } else { updateStage(Stage.SaveFailure); } } // Save recorded pattern as an async task and proceed to next private void startSaveAndFinish() { if (mSaveLockWorker != null && !mSaveLockWorker.isFinished()) { LOG.v("startSaveAndFinish with a running SavePatternWorker."); return; } setPrimaryButtonEnabled(false); if (mSaveLockWorker == null) { mSaveLockWorker = new SaveLockWorker(); mSaveLockWorker.setListener(this::onChosenLockSaveFinished); getFragmentManager() .beginTransaction() .add(mSaveLockWorker, FRAGMENT_TAG_SAVE_PATTERN_WORKER) .commitNow(); } mSaveLockWorker.start(mUserId, mChosenPattern, mCurrentCredential); getToolbar().getProgressBar().setVisible(true); } @VisibleForTesting void onComplete() { if (mCurrentCredential != null) { mCurrentCredential.zeroize(); } getActivity().setResult(Activity.RESULT_OK); getActivity().finish(); } /** * Keep track internally of where the user is in choosing a pattern. */ enum Stage { /** * Initial stage when first launching choose a lock pattern. * Pattern mEnabled, secondary button allow for Cancel, primary button disabled. */ Introduction( R.string.lockpattern_recording_intro_header, SecondaryButtonState.Gone, PrimaryButtonState.ContinueDisabled, /* patternEnabled= */ true), /** * Help screen to show how a valid pattern looks like. * Pattern disabled, primary button shows Ok. No secondary button. */ HelpScreen( R.string.lockpattern_settings_help_how_to_record, SecondaryButtonState.Gone, PrimaryButtonState.Ok, /* patternEnabled= */ false), /** * Invalid pattern is entered, hint message show required number of dots. * Secondary button allows for Retry, primary button disabled. */ ChoiceTooShort( R.string.lockpattern_recording_incorrect_too_short, SecondaryButtonState.Retry, PrimaryButtonState.ContinueDisabled, /* patternEnabled= */ true), /** * First drawing on the pattern is valid, primary button shows Continue, * can proceed to next screen. */ FirstChoiceValid( R.string.lockpattern_recording_intro_header, SecondaryButtonState.Retry, PrimaryButtonState.Continue, /* patternEnabled= */ false), /** * Need to draw pattern again to confirm. * Secondary button allows for Cancel, primary button disabled. */ NeedToConfirm( R.string.lockpattern_need_to_confirm, SecondaryButtonState.Gone, PrimaryButtonState.ConfirmDisabled, /* patternEnabled= */ true), /** * Confirmation of previous drawn pattern failed, didn't enter the same pattern. * Need to re-draw the pattern to match the fist pattern. */ ConfirmWrong( R.string.lockpattern_pattern_wrong, SecondaryButtonState.Gone, PrimaryButtonState.ConfirmDisabled, /* patternEnabled= */ true), /** * Pattern is confirmed after drawing the same pattern twice. * Pattern disabled. */ ChoiceConfirmed( R.string.lockpattern_pattern_confirmed, SecondaryButtonState.Gone, PrimaryButtonState.Confirm, /* patternEnabled= */ false), /** * Error saving pattern. * Pattern disabled, primary button shows Retry, secondary button allows for cancel */ SaveFailure( R.string.error_saving_lockpattern, SecondaryButtonState.Gone, PrimaryButtonState.Retry, /* patternEnabled= */ false); final int mMessageId; final SecondaryButtonState mSecondaryButtonState; final PrimaryButtonState mPrimaryButtonState; final boolean mPatternEnabled; /** * @param messageId The message displayed as instruction. * @param secondaryButtonState The state of the secondary button. * @param primaryButtonState The state of the primary button. * @param patternEnabled Whether the pattern widget is mEnabled. */ Stage(@StringRes int messageId, SecondaryButtonState secondaryButtonState, PrimaryButtonState primaryButtonState, boolean patternEnabled) { this.mMessageId = messageId; this.mSecondaryButtonState = secondaryButtonState; this.mPrimaryButtonState = primaryButtonState; this.mPatternEnabled = patternEnabled; } } /** * The states of the primary footer button. */ enum PrimaryButtonState { Continue(R.string.continue_button_text, true), ContinueDisabled(R.string.continue_button_text, false), Confirm(R.string.lockpattern_confirm_button_text, true), ConfirmDisabled(R.string.lockpattern_confirm_button_text, false), Retry(R.string.lockscreen_retry_button_text, true), Ok(R.string.okay, true); final int mText; final boolean mEnabled; /** * @param text The displayed mText for this mode. * @param enabled Whether the button should be mEnabled. */ PrimaryButtonState(@StringRes int text, boolean enabled) { this.mText = text; this.mEnabled = enabled; } } /** * The states of the secondary footer button. */ enum SecondaryButtonState { Retry(R.string.lockpattern_retry_button_text, true), Gone(ID_EMPTY_MESSAGE, false); final int mTextResId; final boolean mEnabled; /** * @param textId The displayed mText for this mode. * @param enabled Whether the button should be mEnabled. */ SecondaryButtonState(@StringRes int textId, boolean enabled) { this.mTextResId = textId; this.mEnabled = enabled; } } }