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