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