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