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