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