Home | History | Annotate | Download | only in settings
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.settings;
     18 
     19 import com.google.android.collect.Lists;
     20 
     21 import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient;
     22 import com.android.internal.widget.LockPatternUtils;
     23 import com.android.internal.widget.LockPatternView;
     24 import com.android.internal.widget.LockPatternView.Cell;
     25 
     26 import static com.android.internal.widget.LockPatternView.DisplayMode;
     27 
     28 import android.app.Activity;
     29 import android.app.Fragment;
     30 import android.content.Intent;
     31 import android.os.Bundle;
     32 import android.preference.PreferenceActivity;
     33 import android.view.KeyEvent;
     34 import android.view.LayoutInflater;
     35 import android.view.View;
     36 import android.view.ViewGroup;
     37 import android.widget.TextView;
     38 import android.widget.Toast;
     39 
     40 import java.util.ArrayList;
     41 import java.util.Collections;
     42 import java.util.List;
     43 
     44 /**
     45  * If the user has a lock pattern set already, makes them confirm the existing one.
     46  *
     47  * Then, prompts the user to choose a lock pattern:
     48  * - prompts for initial pattern
     49  * - asks for confirmation / restart
     50  * - saves chosen password when confirmed
     51  */
     52 public class ChooseLockPattern extends PreferenceActivity {
     53     /**
     54      * Used by the choose lock pattern wizard to indicate the wizard is
     55      * finished, and each activity in the wizard should finish.
     56      * <p>
     57      * Previously, each activity in the wizard would finish itself after
     58      * starting the next activity. However, this leads to broken 'Back'
     59      * behavior. So, now an activity does not finish itself until it gets this
     60      * result.
     61      */
     62     static final int RESULT_FINISHED = RESULT_FIRST_USER;
     63 
     64     @Override
     65     public Intent getIntent() {
     66         Intent modIntent = new Intent(super.getIntent());
     67         modIntent.putExtra(EXTRA_SHOW_FRAGMENT, ChooseLockPatternFragment.class.getName());
     68         modIntent.putExtra(EXTRA_NO_HEADERS, true);
     69         return modIntent;
     70     }
     71 
     72     @Override
     73     public void onCreate(Bundle savedInstanceState) {
     74         // requestWindowFeature(Window.FEATURE_NO_TITLE);
     75         super.onCreate(savedInstanceState);
     76         CharSequence msg = getText(R.string.lockpassword_choose_your_pattern_header);
     77         showBreadCrumbs(msg, msg);
     78     }
     79 
     80     @Override
     81     public boolean onKeyDown(int keyCode, KeyEvent event) {
     82         // *** TODO ***
     83         // chooseLockPatternFragment.onKeyDown(keyCode, event);
     84         return super.onKeyDown(keyCode, event);
     85     }
     86 
     87     public static class ChooseLockPatternFragment extends Fragment
     88             implements View.OnClickListener {
     89 
     90         public static final int CONFIRM_EXISTING_REQUEST = 55;
     91 
     92         // how long after a confirmation message is shown before moving on
     93         static final int INFORMATION_MSG_TIMEOUT_MS = 3000;
     94 
     95         // how long we wait to clear a wrong pattern
     96         private static final int WRONG_PATTERN_CLEAR_TIMEOUT_MS = 2000;
     97 
     98         private static final int ID_EMPTY_MESSAGE = -1;
     99 
    100         protected TextView mHeaderText;
    101         protected LockPatternView mLockPatternView;
    102         protected TextView mFooterText;
    103         private TextView mFooterLeftButton;
    104         private TextView mFooterRightButton;
    105         protected List<LockPatternView.Cell> mChosenPattern = null;
    106 
    107         /**
    108          * The patten used during the help screen to show how to draw a pattern.
    109          */
    110         private final List<LockPatternView.Cell> mAnimatePattern =
    111                 Collections.unmodifiableList(Lists.newArrayList(
    112                         LockPatternView.Cell.of(0, 0),
    113                         LockPatternView.Cell.of(0, 1),
    114                         LockPatternView.Cell.of(1, 1),
    115                         LockPatternView.Cell.of(2, 1)
    116                 ));
    117 
    118         @Override
    119         public void onActivityResult(int requestCode, int resultCode,
    120                 Intent data) {
    121             super.onActivityResult(requestCode, resultCode, data);
    122             switch (requestCode) {
    123                 case CONFIRM_EXISTING_REQUEST:
    124                     if (resultCode != Activity.RESULT_OK) {
    125                         getActivity().setResult(RESULT_FINISHED);
    126                         getActivity().finish();
    127                     }
    128                     updateStage(Stage.Introduction);
    129                     break;
    130             }
    131         }
    132 
    133         /**
    134          * The pattern listener that responds according to a user choosing a new
    135          * lock pattern.
    136          */
    137         protected LockPatternView.OnPatternListener mChooseNewLockPatternListener =
    138                 new LockPatternView.OnPatternListener() {
    139 
    140                 public void onPatternStart() {
    141                     mLockPatternView.removeCallbacks(mClearPatternRunnable);
    142                     patternInProgress();
    143                 }
    144 
    145                 public void onPatternCleared() {
    146                     mLockPatternView.removeCallbacks(mClearPatternRunnable);
    147                 }
    148 
    149                 public void onPatternDetected(List<LockPatternView.Cell> pattern) {
    150                     if (mUiStage == Stage.NeedToConfirm || mUiStage == Stage.ConfirmWrong) {
    151                         if (mChosenPattern == null) throw new IllegalStateException(
    152                                 "null chosen pattern in stage 'need to confirm");
    153                         if (mChosenPattern.equals(pattern)) {
    154                             updateStage(Stage.ChoiceConfirmed);
    155                         } else {
    156                             updateStage(Stage.ConfirmWrong);
    157                         }
    158                     } else if (mUiStage == Stage.Introduction || mUiStage == Stage.ChoiceTooShort){
    159                         if (pattern.size() < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) {
    160                             updateStage(Stage.ChoiceTooShort);
    161                         } else {
    162                             mChosenPattern = new ArrayList<LockPatternView.Cell>(pattern);
    163                             updateStage(Stage.FirstChoiceValid);
    164                         }
    165                     } else {
    166                         throw new IllegalStateException("Unexpected stage " + mUiStage + " when "
    167                                 + "entering the pattern.");
    168                     }
    169                 }
    170 
    171                 public void onPatternCellAdded(List<Cell> pattern) {
    172 
    173                 }
    174 
    175                 private void patternInProgress() {
    176                     mHeaderText.setText(R.string.lockpattern_recording_inprogress);
    177                     mFooterText.setText("");
    178                     mFooterLeftButton.setEnabled(false);
    179                     mFooterRightButton.setEnabled(false);
    180                 }
    181          };
    182 
    183 
    184         /**
    185          * The states of the left footer button.
    186          */
    187         enum LeftButtonMode {
    188             Cancel(R.string.cancel, true),
    189             CancelDisabled(R.string.cancel, false),
    190             Retry(R.string.lockpattern_retry_button_text, true),
    191             RetryDisabled(R.string.lockpattern_retry_button_text, false),
    192             Gone(ID_EMPTY_MESSAGE, false);
    193 
    194 
    195             /**
    196              * @param text The displayed text for this mode.
    197              * @param enabled Whether the button should be enabled.
    198              */
    199             LeftButtonMode(int text, boolean enabled) {
    200                 this.text = text;
    201                 this.enabled = enabled;
    202             }
    203 
    204             final int text;
    205             final boolean enabled;
    206         }
    207 
    208         /**
    209          * The states of the right button.
    210          */
    211         enum RightButtonMode {
    212             Continue(R.string.lockpattern_continue_button_text, true),
    213             ContinueDisabled(R.string.lockpattern_continue_button_text, false),
    214             Confirm(R.string.lockpattern_confirm_button_text, true),
    215             ConfirmDisabled(R.string.lockpattern_confirm_button_text, false),
    216             Ok(android.R.string.ok, true);
    217 
    218             /**
    219              * @param text The displayed text for this mode.
    220              * @param enabled Whether the button should be enabled.
    221              */
    222             RightButtonMode(int text, boolean enabled) {
    223                 this.text = text;
    224                 this.enabled = enabled;
    225             }
    226 
    227             final int text;
    228             final boolean enabled;
    229         }
    230 
    231         /**
    232          * Keep track internally of where the user is in choosing a pattern.
    233          */
    234         protected enum Stage {
    235 
    236             Introduction(
    237                     R.string.lockpattern_recording_intro_header,
    238                     LeftButtonMode.Cancel, RightButtonMode.ContinueDisabled,
    239                     ID_EMPTY_MESSAGE, true),
    240             HelpScreen(
    241                     R.string.lockpattern_settings_help_how_to_record,
    242                     LeftButtonMode.Gone, RightButtonMode.Ok, ID_EMPTY_MESSAGE, false),
    243             ChoiceTooShort(
    244                     R.string.lockpattern_recording_incorrect_too_short,
    245                     LeftButtonMode.Retry, RightButtonMode.ContinueDisabled,
    246                     ID_EMPTY_MESSAGE, true),
    247             FirstChoiceValid(
    248                     R.string.lockpattern_pattern_entered_header,
    249                     LeftButtonMode.Retry, RightButtonMode.Continue, ID_EMPTY_MESSAGE, false),
    250             NeedToConfirm(
    251                     R.string.lockpattern_need_to_confirm,
    252                     LeftButtonMode.Cancel, RightButtonMode.ConfirmDisabled,
    253                     ID_EMPTY_MESSAGE, true),
    254             ConfirmWrong(
    255                     R.string.lockpattern_need_to_unlock_wrong,
    256                     LeftButtonMode.Cancel, RightButtonMode.ConfirmDisabled,
    257                     ID_EMPTY_MESSAGE, true),
    258             ChoiceConfirmed(
    259                     R.string.lockpattern_pattern_confirmed_header,
    260                     LeftButtonMode.Cancel, RightButtonMode.Confirm, ID_EMPTY_MESSAGE, false);
    261 
    262 
    263             /**
    264              * @param headerMessage The message displayed at the top.
    265              * @param leftMode The mode of the left button.
    266              * @param rightMode The mode of the right button.
    267              * @param footerMessage The footer message.
    268              * @param patternEnabled Whether the pattern widget is enabled.
    269              */
    270             Stage(int headerMessage,
    271                     LeftButtonMode leftMode,
    272                     RightButtonMode rightMode,
    273                     int footerMessage, boolean patternEnabled) {
    274                 this.headerMessage = headerMessage;
    275                 this.leftMode = leftMode;
    276                 this.rightMode = rightMode;
    277                 this.footerMessage = footerMessage;
    278                 this.patternEnabled = patternEnabled;
    279             }
    280 
    281             final int headerMessage;
    282             final LeftButtonMode leftMode;
    283             final RightButtonMode rightMode;
    284             final int footerMessage;
    285             final boolean patternEnabled;
    286         }
    287 
    288         private Stage mUiStage = Stage.Introduction;
    289 
    290         private Runnable mClearPatternRunnable = new Runnable() {
    291             public void run() {
    292                 mLockPatternView.clearPattern();
    293             }
    294         };
    295 
    296         private ChooseLockSettingsHelper mChooseLockSettingsHelper;
    297 
    298         private static final String KEY_UI_STAGE = "uiStage";
    299         private static final String KEY_PATTERN_CHOICE = "chosenPattern";
    300 
    301         @Override
    302         public void onCreate(Bundle savedInstanceState) {
    303             super.onCreate(savedInstanceState);
    304             mChooseLockSettingsHelper = new ChooseLockSettingsHelper(getActivity());
    305         }
    306 
    307         @Override
    308         public View onCreateView(LayoutInflater inflater, ViewGroup container,
    309                 Bundle savedInstanceState) {
    310 
    311             // setupViews()
    312             View view = inflater.inflate(R.layout.choose_lock_pattern, null);
    313             mHeaderText = (TextView) view.findViewById(R.id.headerText);
    314             mLockPatternView = (LockPatternView) view.findViewById(R.id.lockPattern);
    315             mLockPatternView.setOnPatternListener(mChooseNewLockPatternListener);
    316             mLockPatternView.setTactileFeedbackEnabled(
    317                     mChooseLockSettingsHelper.utils().isTactileFeedbackEnabled());
    318 
    319             mFooterText = (TextView) view.findViewById(R.id.footerText);
    320 
    321             mFooterLeftButton = (TextView) view.findViewById(R.id.footerLeftButton);
    322             mFooterRightButton = (TextView) view.findViewById(R.id.footerRightButton);
    323 
    324             mFooterLeftButton.setOnClickListener(this);
    325             mFooterRightButton.setOnClickListener(this);
    326 
    327             // make it so unhandled touch events within the unlock screen go to the
    328             // lock pattern view.
    329             final LinearLayoutWithDefaultTouchRecepient topLayout
    330                     = (LinearLayoutWithDefaultTouchRecepient) view.findViewById(
    331                     R.id.topLayout);
    332             topLayout.setDefaultTouchRecepient(mLockPatternView);
    333 
    334             final boolean confirmCredentials = getActivity().getIntent()
    335                     .getBooleanExtra("confirm_credentials", false);
    336 
    337             if (savedInstanceState == null) {
    338                 if (confirmCredentials) {
    339                     // first launch. As a security measure, we're in NeedToConfirm mode until we
    340                     // know there isn't an existing password or the user confirms their password.
    341                     updateStage(Stage.NeedToConfirm);
    342                     boolean launchedConfirmationActivity =
    343                         mChooseLockSettingsHelper.launchConfirmationActivity(
    344                                 CONFIRM_EXISTING_REQUEST, null, null);
    345                     if (!launchedConfirmationActivity) {
    346                         updateStage(Stage.Introduction);
    347                     }
    348                 } else {
    349                     updateStage(Stage.Introduction);
    350                 }
    351             } else {
    352                 // restore from previous state
    353                 final String patternString = savedInstanceState.getString(KEY_PATTERN_CHOICE);
    354                 if (patternString != null) {
    355                     mChosenPattern = LockPatternUtils.stringToPattern(patternString);
    356                 }
    357                 updateStage(Stage.values()[savedInstanceState.getInt(KEY_UI_STAGE)]);
    358             }
    359             return view;
    360         }
    361 
    362         public void onClick(View v) {
    363             if (v == mFooterLeftButton) {
    364                 if (mUiStage.leftMode == LeftButtonMode.Retry) {
    365                     mChosenPattern = null;
    366                     mLockPatternView.clearPattern();
    367                     updateStage(Stage.Introduction);
    368                 } else if (mUiStage.leftMode == LeftButtonMode.Cancel) {
    369                     // They are canceling the entire wizard
    370                     getActivity().setResult(RESULT_FINISHED);
    371                     getActivity().finish();
    372                 } else {
    373                     throw new IllegalStateException("left footer button pressed, but stage of " +
    374                         mUiStage + " doesn't make sense");
    375                 }
    376             } else if (v == mFooterRightButton) {
    377 
    378                 if (mUiStage.rightMode == RightButtonMode.Continue) {
    379                     if (mUiStage != Stage.FirstChoiceValid) {
    380                         throw new IllegalStateException("expected ui stage " + Stage.FirstChoiceValid
    381                                 + " when button is " + RightButtonMode.Continue);
    382                     }
    383                     updateStage(Stage.NeedToConfirm);
    384                 } else if (mUiStage.rightMode == RightButtonMode.Confirm) {
    385                     if (mUiStage != Stage.ChoiceConfirmed) {
    386                         throw new IllegalStateException("expected ui stage " + Stage.ChoiceConfirmed
    387                                 + " when button is " + RightButtonMode.Confirm);
    388                     }
    389                     saveChosenPatternAndFinish();
    390                 } else if (mUiStage.rightMode == RightButtonMode.Ok) {
    391                     if (mUiStage != Stage.HelpScreen) {
    392                         throw new IllegalStateException("Help screen is only mode with ok button, but " +
    393                                 "stage is " + mUiStage);
    394                     }
    395                     mLockPatternView.clearPattern();
    396                     mLockPatternView.setDisplayMode(DisplayMode.Correct);
    397                     updateStage(Stage.Introduction);
    398                 }
    399             }
    400         }
    401 
    402         public boolean onKeyDown(int keyCode, KeyEvent event) {
    403             if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
    404                 if (mUiStage == Stage.HelpScreen) {
    405                     updateStage(Stage.Introduction);
    406                     return true;
    407                 }
    408             }
    409             if (keyCode == KeyEvent.KEYCODE_MENU && mUiStage == Stage.Introduction) {
    410                 updateStage(Stage.HelpScreen);
    411                 return true;
    412             }
    413             return false;
    414         }
    415 
    416         public void onSaveInstanceState(Bundle outState) {
    417             super.onSaveInstanceState(outState);
    418 
    419             outState.putInt(KEY_UI_STAGE, mUiStage.ordinal());
    420             if (mChosenPattern != null) {
    421                 outState.putString(KEY_PATTERN_CHOICE,
    422                         LockPatternUtils.patternToString(mChosenPattern));
    423             }
    424         }
    425 
    426         /**
    427          * Updates the messages and buttons appropriate to what stage the user
    428          * is at in choosing a view.  This doesn't handle clearing out the pattern;
    429          * the pattern is expected to be in the right state.
    430          * @param stage
    431          */
    432         protected void updateStage(Stage stage) {
    433 
    434             mUiStage = stage;
    435 
    436             // header text, footer text, visibility and
    437             // enabled state all known from the stage
    438             if (stage == Stage.ChoiceTooShort) {
    439                 mHeaderText.setText(
    440                         getResources().getString(
    441                                 stage.headerMessage,
    442                                 LockPatternUtils.MIN_LOCK_PATTERN_SIZE));
    443             } else {
    444                 mHeaderText.setText(stage.headerMessage);
    445             }
    446             if (stage.footerMessage == ID_EMPTY_MESSAGE) {
    447                 mFooterText.setText("");
    448             } else {
    449                 mFooterText.setText(stage.footerMessage);
    450             }
    451 
    452             if (stage.leftMode == LeftButtonMode.Gone) {
    453                 mFooterLeftButton.setVisibility(View.GONE);
    454             } else {
    455                 mFooterLeftButton.setVisibility(View.VISIBLE);
    456                 mFooterLeftButton.setText(stage.leftMode.text);
    457                 mFooterLeftButton.setEnabled(stage.leftMode.enabled);
    458             }
    459 
    460             mFooterRightButton.setText(stage.rightMode.text);
    461             mFooterRightButton.setEnabled(stage.rightMode.enabled);
    462 
    463             // same for whether the patten is enabled
    464             if (stage.patternEnabled) {
    465                 mLockPatternView.enableInput();
    466             } else {
    467                 mLockPatternView.disableInput();
    468             }
    469 
    470             // the rest of the stuff varies enough that it is easier just to handle
    471             // on a case by case basis.
    472             mLockPatternView.setDisplayMode(DisplayMode.Correct);
    473 
    474             switch (mUiStage) {
    475                 case Introduction:
    476                     mLockPatternView.clearPattern();
    477                     break;
    478                 case HelpScreen:
    479                     mLockPatternView.setPattern(DisplayMode.Animate, mAnimatePattern);
    480                     break;
    481                 case ChoiceTooShort:
    482                     mLockPatternView.setDisplayMode(DisplayMode.Wrong);
    483                     postClearPatternRunnable();
    484                     break;
    485                 case FirstChoiceValid:
    486                     break;
    487                 case NeedToConfirm:
    488                     mLockPatternView.clearPattern();
    489                     break;
    490                 case ConfirmWrong:
    491                     mLockPatternView.setDisplayMode(DisplayMode.Wrong);
    492                     postClearPatternRunnable();
    493                     break;
    494                 case ChoiceConfirmed:
    495                     break;
    496             }
    497         }
    498 
    499 
    500         // clear the wrong pattern unless they have started a new one
    501         // already
    502         private void postClearPatternRunnable() {
    503             mLockPatternView.removeCallbacks(mClearPatternRunnable);
    504             mLockPatternView.postDelayed(mClearPatternRunnable, WRONG_PATTERN_CLEAR_TIMEOUT_MS);
    505         }
    506 
    507         private void saveChosenPatternAndFinish() {
    508             LockPatternUtils utils = mChooseLockSettingsHelper.utils();
    509             final boolean lockVirgin = !utils.isPatternEverChosen();
    510 
    511             final boolean isFallback = getActivity().getIntent()
    512                 .getBooleanExtra(LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK, false);
    513             utils.saveLockPattern(mChosenPattern, isFallback);
    514             utils.setLockPatternEnabled(true);
    515 
    516             Toast.makeText(getActivity(), R.string.lock_setup, Toast.LENGTH_SHORT).show();
    517 
    518             if (lockVirgin) {
    519                 utils.setVisiblePatternEnabled(true);
    520                 utils.setTactileFeedbackEnabled(false);
    521             }
    522 
    523             getActivity().setResult(RESULT_FINISHED);
    524             getActivity().finish();
    525         }
    526     }
    527 }
    528