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