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