| /* |
| * Copyright (C) 2007 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; |
| |
| import com.google.android.collect.Lists; |
| |
| import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient; |
| import com.android.internal.widget.LockPatternUtils; |
| import com.android.internal.widget.LockPatternView; |
| import com.android.internal.widget.LockPatternView.Cell; |
| |
| import static com.android.internal.widget.LockPatternView.DisplayMode; |
| |
| import android.app.Activity; |
| import android.app.Fragment; |
| import android.content.Intent; |
| import android.os.Bundle; |
| import android.preference.PreferenceActivity; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.TextView; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| /** |
| * If the user has a lock pattern set already, makes them confirm the existing one. |
| * |
| * Then, prompts the user to choose a lock pattern: |
| * - prompts for initial pattern |
| * - asks for confirmation / restart |
| * - saves chosen password when confirmed |
| */ |
| public class ChooseLockPattern extends PreferenceActivity { |
| /** |
| * Used by the choose lock pattern wizard to indicate the wizard is |
| * finished, and each activity in the wizard should finish. |
| * <p> |
| * Previously, each activity in the wizard would finish itself after |
| * starting the next activity. However, this leads to broken 'Back' |
| * behavior. So, now an activity does not finish itself until it gets this |
| * result. |
| */ |
| static final int RESULT_FINISHED = RESULT_FIRST_USER; |
| |
| @Override |
| public Intent getIntent() { |
| Intent modIntent = new Intent(super.getIntent()); |
| modIntent.putExtra(EXTRA_SHOW_FRAGMENT, ChooseLockPatternFragment.class.getName()); |
| modIntent.putExtra(EXTRA_NO_HEADERS, true); |
| return modIntent; |
| } |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| // requestWindowFeature(Window.FEATURE_NO_TITLE); |
| super.onCreate(savedInstanceState); |
| CharSequence msg = getText(R.string.lockpassword_choose_your_pattern_header); |
| showBreadCrumbs(msg, msg); |
| } |
| |
| @Override |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| // *** TODO *** |
| // chooseLockPatternFragment.onKeyDown(keyCode, event); |
| return super.onKeyDown(keyCode, event); |
| } |
| |
| public static class ChooseLockPatternFragment extends Fragment |
| implements View.OnClickListener { |
| |
| public static final int CONFIRM_EXISTING_REQUEST = 55; |
| |
| // how long after a confirmation message is shown before moving on |
| static final int INFORMATION_MSG_TIMEOUT_MS = 3000; |
| |
| // how long we wait to clear a wrong pattern |
| private static final int WRONG_PATTERN_CLEAR_TIMEOUT_MS = 2000; |
| |
| private static final int ID_EMPTY_MESSAGE = -1; |
| |
| protected TextView mHeaderText; |
| protected LockPatternView mLockPatternView; |
| protected TextView mFooterText; |
| private TextView mFooterLeftButton; |
| private TextView mFooterRightButton; |
| protected List<LockPatternView.Cell> mChosenPattern = null; |
| |
| /** |
| * 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) |
| )); |
| |
| @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(); |
| } |
| updateStage(Stage.Introduction); |
| break; |
| } |
| } |
| |
| /** |
| * The pattern listener that responds according to a user choosing a new |
| * lock pattern. |
| */ |
| protected LockPatternView.OnPatternListener mChooseNewLockPatternListener = |
| new LockPatternView.OnPatternListener() { |
| |
| public void onPatternStart() { |
| mLockPatternView.removeCallbacks(mClearPatternRunnable); |
| patternInProgress(); |
| } |
| |
| public void onPatternCleared() { |
| mLockPatternView.removeCallbacks(mClearPatternRunnable); |
| } |
| |
| public void onPatternDetected(List<LockPatternView.Cell> pattern) { |
| if (mUiStage == Stage.NeedToConfirm || mUiStage == Stage.ConfirmWrong) { |
| if (mChosenPattern == null) throw new IllegalStateException( |
| "null chosen pattern in stage 'need to confirm"); |
| if (mChosenPattern.equals(pattern)) { |
| updateStage(Stage.ChoiceConfirmed); |
| } else { |
| updateStage(Stage.ConfirmWrong); |
| } |
| } else if (mUiStage == Stage.Introduction || mUiStage == Stage.ChoiceTooShort){ |
| if (pattern.size() < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) { |
| updateStage(Stage.ChoiceTooShort); |
| } else { |
| mChosenPattern = new ArrayList<LockPatternView.Cell>(pattern); |
| updateStage(Stage.FirstChoiceValid); |
| } |
| } else { |
| throw new IllegalStateException("Unexpected stage " + mUiStage + " when " |
| + "entering the pattern."); |
| } |
| } |
| |
| public void onPatternCellAdded(List<Cell> pattern) { |
| |
| } |
| |
| private void patternInProgress() { |
| mHeaderText.setText(R.string.lockpattern_recording_inprogress); |
| mFooterText.setText(""); |
| mFooterLeftButton.setEnabled(false); |
| mFooterRightButton.setEnabled(false); |
| } |
| }; |
| |
| |
| /** |
| * The states of the left footer button. |
| */ |
| enum LeftButtonMode { |
| Cancel(R.string.cancel, true), |
| CancelDisabled(R.string.cancel, false), |
| Retry(R.string.lockpattern_retry_button_text, true), |
| RetryDisabled(R.string.lockpattern_retry_button_text, false), |
| Gone(ID_EMPTY_MESSAGE, false); |
| |
| |
| /** |
| * @param text The displayed text for this mode. |
| * @param enabled Whether the button should be enabled. |
| */ |
| LeftButtonMode(int text, boolean enabled) { |
| this.text = text; |
| this.enabled = enabled; |
| } |
| |
| final int text; |
| final boolean enabled; |
| } |
| |
| /** |
| * The states of the right button. |
| */ |
| enum RightButtonMode { |
| Continue(R.string.lockpattern_continue_button_text, true), |
| ContinueDisabled(R.string.lockpattern_continue_button_text, false), |
| Confirm(R.string.lockpattern_confirm_button_text, true), |
| ConfirmDisabled(R.string.lockpattern_confirm_button_text, false), |
| Ok(android.R.string.ok, true); |
| |
| /** |
| * @param text The displayed text for this mode. |
| * @param enabled Whether the button should be enabled. |
| */ |
| RightButtonMode(int text, boolean enabled) { |
| this.text = text; |
| this.enabled = enabled; |
| } |
| |
| final int text; |
| final boolean enabled; |
| } |
| |
| /** |
| * Keep track internally of where the user is in choosing a pattern. |
| */ |
| protected enum Stage { |
| |
| Introduction( |
| R.string.lockpattern_recording_intro_header, |
| LeftButtonMode.Cancel, RightButtonMode.ContinueDisabled, |
| ID_EMPTY_MESSAGE, true), |
| HelpScreen( |
| R.string.lockpattern_settings_help_how_to_record, |
| LeftButtonMode.Gone, RightButtonMode.Ok, ID_EMPTY_MESSAGE, false), |
| ChoiceTooShort( |
| R.string.lockpattern_recording_incorrect_too_short, |
| LeftButtonMode.Retry, RightButtonMode.ContinueDisabled, |
| ID_EMPTY_MESSAGE, true), |
| FirstChoiceValid( |
| R.string.lockpattern_pattern_entered_header, |
| LeftButtonMode.Retry, RightButtonMode.Continue, ID_EMPTY_MESSAGE, false), |
| NeedToConfirm( |
| R.string.lockpattern_need_to_confirm, |
| LeftButtonMode.Cancel, RightButtonMode.ConfirmDisabled, |
| ID_EMPTY_MESSAGE, true), |
| ConfirmWrong( |
| R.string.lockpattern_need_to_unlock_wrong, |
| LeftButtonMode.Cancel, RightButtonMode.ConfirmDisabled, |
| ID_EMPTY_MESSAGE, true), |
| ChoiceConfirmed( |
| R.string.lockpattern_pattern_confirmed_header, |
| LeftButtonMode.Cancel, RightButtonMode.Confirm, ID_EMPTY_MESSAGE, false); |
| |
| |
| /** |
| * @param headerMessage The message displayed at the top. |
| * @param leftMode The mode of the left button. |
| * @param rightMode The mode of the right button. |
| * @param footerMessage The footer message. |
| * @param patternEnabled Whether the pattern widget is enabled. |
| */ |
| Stage(int headerMessage, |
| LeftButtonMode leftMode, |
| RightButtonMode rightMode, |
| int footerMessage, boolean patternEnabled) { |
| this.headerMessage = headerMessage; |
| this.leftMode = leftMode; |
| this.rightMode = rightMode; |
| this.footerMessage = footerMessage; |
| this.patternEnabled = patternEnabled; |
| } |
| |
| final int headerMessage; |
| final LeftButtonMode leftMode; |
| final RightButtonMode rightMode; |
| final int footerMessage; |
| final boolean patternEnabled; |
| } |
| |
| private Stage mUiStage = Stage.Introduction; |
| |
| private Runnable mClearPatternRunnable = new Runnable() { |
| public void run() { |
| mLockPatternView.clearPattern(); |
| } |
| }; |
| |
| private ChooseLockSettingsHelper mChooseLockSettingsHelper; |
| |
| private static final String KEY_UI_STAGE = "uiStage"; |
| private static final String KEY_PATTERN_CHOICE = "chosenPattern"; |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| mChooseLockSettingsHelper = new ChooseLockSettingsHelper(getActivity()); |
| } |
| |
| @Override |
| public View onCreateView(LayoutInflater inflater, ViewGroup container, |
| Bundle savedInstanceState) { |
| |
| // setupViews() |
| View view = inflater.inflate(R.layout.choose_lock_pattern, null); |
| mHeaderText = (TextView) view.findViewById(R.id.headerText); |
| mLockPatternView = (LockPatternView) view.findViewById(R.id.lockPattern); |
| mLockPatternView.setOnPatternListener(mChooseNewLockPatternListener); |
| mLockPatternView.setTactileFeedbackEnabled( |
| mChooseLockSettingsHelper.utils().isTactileFeedbackEnabled()); |
| |
| mFooterText = (TextView) view.findViewById(R.id.footerText); |
| |
| mFooterLeftButton = (TextView) view.findViewById(R.id.footerLeftButton); |
| mFooterRightButton = (TextView) view.findViewById(R.id.footerRightButton); |
| |
| mFooterLeftButton.setOnClickListener(this); |
| mFooterRightButton.setOnClickListener(this); |
| |
| // make it so unhandled touch events within the unlock screen go to the |
| // lock pattern view. |
| final LinearLayoutWithDefaultTouchRecepient topLayout |
| = (LinearLayoutWithDefaultTouchRecepient) view.findViewById( |
| R.id.topLayout); |
| topLayout.setDefaultTouchRecepient(mLockPatternView); |
| |
| final boolean confirmCredentials = getActivity().getIntent() |
| .getBooleanExtra("confirm_credentials", false); |
| |
| if (savedInstanceState == null) { |
| if (confirmCredentials) { |
| // first launch. As a security measure, we're in NeedToConfirm mode until we |
| // know there isn't an existing password or the user confirms their password. |
| updateStage(Stage.NeedToConfirm); |
| boolean launchedConfirmationActivity = |
| mChooseLockSettingsHelper.launchConfirmationActivity( |
| CONFIRM_EXISTING_REQUEST, null, null); |
| if (!launchedConfirmationActivity) { |
| updateStage(Stage.Introduction); |
| } |
| } else { |
| updateStage(Stage.Introduction); |
| } |
| } else { |
| // restore from previous state |
| final String patternString = savedInstanceState.getString(KEY_PATTERN_CHOICE); |
| if (patternString != null) { |
| mChosenPattern = LockPatternUtils.stringToPattern(patternString); |
| } |
| updateStage(Stage.values()[savedInstanceState.getInt(KEY_UI_STAGE)]); |
| } |
| return view; |
| } |
| |
| public void onClick(View v) { |
| if (v == mFooterLeftButton) { |
| if (mUiStage.leftMode == LeftButtonMode.Retry) { |
| mChosenPattern = null; |
| mLockPatternView.clearPattern(); |
| updateStage(Stage.Introduction); |
| } else if (mUiStage.leftMode == LeftButtonMode.Cancel) { |
| // They are canceling the entire wizard |
| getActivity().setResult(RESULT_FINISHED); |
| getActivity().finish(); |
| } else { |
| throw new IllegalStateException("left footer button pressed, but stage of " + |
| mUiStage + " doesn't make sense"); |
| } |
| } else if (v == mFooterRightButton) { |
| |
| if (mUiStage.rightMode == RightButtonMode.Continue) { |
| if (mUiStage != Stage.FirstChoiceValid) { |
| throw new IllegalStateException("expected ui stage " + Stage.FirstChoiceValid |
| + " when button is " + RightButtonMode.Continue); |
| } |
| updateStage(Stage.NeedToConfirm); |
| } else if (mUiStage.rightMode == RightButtonMode.Confirm) { |
| if (mUiStage != Stage.ChoiceConfirmed) { |
| throw new IllegalStateException("expected ui stage " + Stage.ChoiceConfirmed |
| + " when button is " + RightButtonMode.Confirm); |
| } |
| saveChosenPatternAndFinish(); |
| } else if (mUiStage.rightMode == RightButtonMode.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); |
| } |
| } |
| } |
| |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { |
| if (mUiStage == Stage.HelpScreen) { |
| updateStage(Stage.Introduction); |
| return true; |
| } |
| } |
| if (keyCode == KeyEvent.KEYCODE_MENU && mUiStage == Stage.Introduction) { |
| updateStage(Stage.HelpScreen); |
| return true; |
| } |
| return false; |
| } |
| |
| public void onSaveInstanceState(Bundle outState) { |
| super.onSaveInstanceState(outState); |
| |
| outState.putInt(KEY_UI_STAGE, mUiStage.ordinal()); |
| if (mChosenPattern != null) { |
| outState.putString(KEY_PATTERN_CHOICE, |
| LockPatternUtils.patternToString(mChosenPattern)); |
| } |
| } |
| |
| /** |
| * Updates the messages and buttons appropriate to what stage the user |
| * is at in choosing a view. This doesn't handle clearing out the pattern; |
| * the pattern is expected to be in the right state. |
| * @param stage |
| */ |
| protected void updateStage(Stage stage) { |
| final Stage previousStage = mUiStage; |
| |
| mUiStage = stage; |
| |
| // header text, footer text, visibility and |
| // enabled state all known from the stage |
| if (stage == Stage.ChoiceTooShort) { |
| mHeaderText.setText( |
| getResources().getString( |
| stage.headerMessage, |
| LockPatternUtils.MIN_LOCK_PATTERN_SIZE)); |
| } else { |
| mHeaderText.setText(stage.headerMessage); |
| } |
| if (stage.footerMessage == ID_EMPTY_MESSAGE) { |
| mFooterText.setText(""); |
| } else { |
| mFooterText.setText(stage.footerMessage); |
| } |
| |
| if (stage.leftMode == LeftButtonMode.Gone) { |
| mFooterLeftButton.setVisibility(View.GONE); |
| } else { |
| mFooterLeftButton.setVisibility(View.VISIBLE); |
| mFooterLeftButton.setText(stage.leftMode.text); |
| mFooterLeftButton.setEnabled(stage.leftMode.enabled); |
| } |
| |
| mFooterRightButton.setText(stage.rightMode.text); |
| mFooterRightButton.setEnabled(stage.rightMode.enabled); |
| |
| // same for whether the patten is enabled |
| if (stage.patternEnabled) { |
| 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; |
| } |
| |
| // If the stage changed, announce the header for accessibility. This |
| // is a no-op when accessibility is disabled. |
| if (previousStage != stage) { |
| mHeaderText.announceForAccessibility(mHeaderText.getText()); |
| } |
| } |
| |
| |
| // clear the wrong pattern unless they have started a new one |
| // already |
| private void postClearPatternRunnable() { |
| mLockPatternView.removeCallbacks(mClearPatternRunnable); |
| mLockPatternView.postDelayed(mClearPatternRunnable, WRONG_PATTERN_CLEAR_TIMEOUT_MS); |
| } |
| |
| private void saveChosenPatternAndFinish() { |
| LockPatternUtils utils = mChooseLockSettingsHelper.utils(); |
| final boolean lockVirgin = !utils.isPatternEverChosen(); |
| |
| final boolean isFallback = getActivity().getIntent() |
| .getBooleanExtra(LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK, false); |
| utils.saveLockPattern(mChosenPattern, isFallback); |
| utils.setLockPatternEnabled(true); |
| |
| if (lockVirgin) { |
| utils.setVisiblePatternEnabled(true); |
| } |
| |
| getActivity().setResult(RESULT_FINISHED); |
| getActivity().finish(); |
| } |
| } |
| } |