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