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