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