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