ChooseLockPattern.java revision 0698a216311acd9ab52224602d1a75c08bf89f04
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.google.android.collect.Lists;
20import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient;
21import com.android.internal.widget.LockPatternUtils;
22import com.android.internal.widget.LockPatternView;
23import com.android.internal.widget.LockPatternView.Cell;
24import com.android.settings.notification.RedactionInterstitial;
25
26import static com.android.internal.widget.LockPatternView.DisplayMode;
27
28import android.app.Activity;
29import android.app.Fragment;
30import android.content.ContentResolver;
31import android.content.Context;
32import android.content.Intent;
33import android.os.Bundle;
34import android.provider.Settings;
35import android.view.KeyEvent;
36import android.view.LayoutInflater;
37import android.view.View;
38import android.view.ViewGroup;
39import android.widget.TextView;
40
41import java.util.ArrayList;
42import java.util.Collections;
43import 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 */
53public 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