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