blob: bfb5e8906545153296c458a7075ae56f917db09e [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.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<LockPatternView.Cell> 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<LockPatternView.Cell> 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<Cell> pattern) {
}
private void handleConfirmPattern(List<LockPatternView.Cell> 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<LockPatternView.Cell> 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<MenuItem> 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;
}
}
}