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            if (!(getActivity() instanceof ChooseLockPattern)) {
312                throw new SecurityException("Fragment contained in wrong activity");
313            }
314        }
315
316        @Override
317        public View onCreateView(LayoutInflater inflater, ViewGroup container,
318                Bundle savedInstanceState) {
319
320            // setupViews()
321            View view = inflater.inflate(R.layout.choose_lock_pattern, null);
322            mHeaderText = (TextView) view.findViewById(R.id.headerText);
323            mLockPatternView = (LockPatternView) view.findViewById(R.id.lockPattern);
324            mLockPatternView.setOnPatternListener(mChooseNewLockPatternListener);
325            mLockPatternView.setTactileFeedbackEnabled(
326                    mChooseLockSettingsHelper.utils().isTactileFeedbackEnabled());
327
328            mFooterText = (TextView) view.findViewById(R.id.footerText);
329
330            mFooterLeftButton = (TextView) view.findViewById(R.id.footerLeftButton);
331            mFooterRightButton = (TextView) view.findViewById(R.id.footerRightButton);
332
333            mFooterLeftButton.setOnClickListener(this);
334            mFooterRightButton.setOnClickListener(this);
335
336            // make it so unhandled touch events within the unlock screen go to the
337            // lock pattern view.
338            final LinearLayoutWithDefaultTouchRecepient topLayout
339                    = (LinearLayoutWithDefaultTouchRecepient) view.findViewById(
340                    R.id.topLayout);
341            topLayout.setDefaultTouchRecepient(mLockPatternView);
342
343            final boolean confirmCredentials = getActivity().getIntent()
344                    .getBooleanExtra("confirm_credentials", true);
345
346            if (savedInstanceState == null) {
347                if (confirmCredentials) {
348                    // first launch. As a security measure, we're in NeedToConfirm mode until we
349                    // know there isn't an existing password or the user confirms their password.
350                    updateStage(Stage.NeedToConfirm);
351                    boolean launchedConfirmationActivity =
352                        mChooseLockSettingsHelper.launchConfirmationActivity(
353                                CONFIRM_EXISTING_REQUEST, null, null);
354                    if (!launchedConfirmationActivity) {
355                        updateStage(Stage.Introduction);
356                    }
357                } else {
358                    updateStage(Stage.Introduction);
359                }
360            } else {
361                // restore from previous state
362                final String patternString = savedInstanceState.getString(KEY_PATTERN_CHOICE);
363                if (patternString != null) {
364                    mChosenPattern = LockPatternUtils.stringToPattern(patternString);
365                }
366                updateStage(Stage.values()[savedInstanceState.getInt(KEY_UI_STAGE)]);
367            }
368            return view;
369        }
370
371        public void onClick(View v) {
372            if (v == mFooterLeftButton) {
373                if (mUiStage.leftMode == LeftButtonMode.Retry) {
374                    mChosenPattern = null;
375                    mLockPatternView.clearPattern();
376                    updateStage(Stage.Introduction);
377                } else if (mUiStage.leftMode == LeftButtonMode.Cancel) {
378                    // They are canceling the entire wizard
379                    getActivity().setResult(RESULT_FINISHED);
380                    getActivity().finish();
381                } else {
382                    throw new IllegalStateException("left footer button pressed, but stage of " +
383                        mUiStage + " doesn't make sense");
384                }
385            } else if (v == mFooterRightButton) {
386
387                if (mUiStage.rightMode == RightButtonMode.Continue) {
388                    if (mUiStage != Stage.FirstChoiceValid) {
389                        throw new IllegalStateException("expected ui stage " + Stage.FirstChoiceValid
390                                + " when button is " + RightButtonMode.Continue);
391                    }
392                    updateStage(Stage.NeedToConfirm);
393                } else if (mUiStage.rightMode == RightButtonMode.Confirm) {
394                    if (mUiStage != Stage.ChoiceConfirmed) {
395                        throw new IllegalStateException("expected ui stage " + Stage.ChoiceConfirmed
396                                + " when button is " + RightButtonMode.Confirm);
397                    }
398                    saveChosenPatternAndFinish();
399                } else if (mUiStage.rightMode == RightButtonMode.Ok) {
400                    if (mUiStage != Stage.HelpScreen) {
401                        throw new IllegalStateException("Help screen is only mode with ok button, but " +
402                                "stage is " + mUiStage);
403                    }
404                    mLockPatternView.clearPattern();
405                    mLockPatternView.setDisplayMode(DisplayMode.Correct);
406                    updateStage(Stage.Introduction);
407                }
408            }
409        }
410
411        public boolean onKeyDown(int keyCode, KeyEvent event) {
412            if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
413                if (mUiStage == Stage.HelpScreen) {
414                    updateStage(Stage.Introduction);
415                    return true;
416                }
417            }
418            if (keyCode == KeyEvent.KEYCODE_MENU && mUiStage == Stage.Introduction) {
419                updateStage(Stage.HelpScreen);
420                return true;
421            }
422            return false;
423        }
424
425        public void onSaveInstanceState(Bundle outState) {
426            super.onSaveInstanceState(outState);
427
428            outState.putInt(KEY_UI_STAGE, mUiStage.ordinal());
429            if (mChosenPattern != null) {
430                outState.putString(KEY_PATTERN_CHOICE,
431                        LockPatternUtils.patternToString(mChosenPattern));
432            }
433        }
434
435        /**
436         * Updates the messages and buttons appropriate to what stage the user
437         * is at in choosing a view.  This doesn't handle clearing out the pattern;
438         * the pattern is expected to be in the right state.
439         * @param stage
440         */
441        protected void updateStage(Stage stage) {
442            final Stage previousStage = mUiStage;
443
444            mUiStage = stage;
445
446            // header text, footer text, visibility and
447            // enabled state all known from the stage
448            if (stage == Stage.ChoiceTooShort) {
449                mHeaderText.setText(
450                        getResources().getString(
451                                stage.headerMessage,
452                                LockPatternUtils.MIN_LOCK_PATTERN_SIZE));
453            } else {
454                mHeaderText.setText(stage.headerMessage);
455            }
456            if (stage.footerMessage == ID_EMPTY_MESSAGE) {
457                mFooterText.setText("");
458            } else {
459                mFooterText.setText(stage.footerMessage);
460            }
461
462            if (stage.leftMode == LeftButtonMode.Gone) {
463                mFooterLeftButton.setVisibility(View.GONE);
464            } else {
465                mFooterLeftButton.setVisibility(View.VISIBLE);
466                mFooterLeftButton.setText(stage.leftMode.text);
467                mFooterLeftButton.setEnabled(stage.leftMode.enabled);
468            }
469
470            mFooterRightButton.setText(stage.rightMode.text);
471            mFooterRightButton.setEnabled(stage.rightMode.enabled);
472
473            // same for whether the patten is enabled
474            if (stage.patternEnabled) {
475                mLockPatternView.enableInput();
476            } else {
477                mLockPatternView.disableInput();
478            }
479
480            // the rest of the stuff varies enough that it is easier just to handle
481            // on a case by case basis.
482            mLockPatternView.setDisplayMode(DisplayMode.Correct);
483
484            switch (mUiStage) {
485                case Introduction:
486                    mLockPatternView.clearPattern();
487                    break;
488                case HelpScreen:
489                    mLockPatternView.setPattern(DisplayMode.Animate, mAnimatePattern);
490                    break;
491                case ChoiceTooShort:
492                    mLockPatternView.setDisplayMode(DisplayMode.Wrong);
493                    postClearPatternRunnable();
494                    break;
495                case FirstChoiceValid:
496                    break;
497                case NeedToConfirm:
498                    mLockPatternView.clearPattern();
499                    break;
500                case ConfirmWrong:
501                    mLockPatternView.setDisplayMode(DisplayMode.Wrong);
502                    postClearPatternRunnable();
503                    break;
504                case ChoiceConfirmed:
505                    break;
506            }
507
508            // If the stage changed, announce the header for accessibility. This
509            // is a no-op when accessibility is disabled.
510            if (previousStage != stage) {
511                mHeaderText.announceForAccessibility(mHeaderText.getText());
512            }
513        }
514
515
516        // clear the wrong pattern unless they have started a new one
517        // already
518        private void postClearPatternRunnable() {
519            mLockPatternView.removeCallbacks(mClearPatternRunnable);
520            mLockPatternView.postDelayed(mClearPatternRunnable, WRONG_PATTERN_CLEAR_TIMEOUT_MS);
521        }
522
523        private void saveChosenPatternAndFinish() {
524            LockPatternUtils utils = mChooseLockSettingsHelper.utils();
525            final boolean lockVirgin = !utils.isPatternEverChosen();
526
527            final boolean isFallback = getActivity().getIntent()
528                .getBooleanExtra(LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK, false);
529            utils.saveLockPattern(mChosenPattern, isFallback);
530            utils.setLockPatternEnabled(true);
531
532            if (lockVirgin) {
533                utils.setVisiblePatternEnabled(true);
534            }
535
536            getActivity().setResult(RESULT_FINISHED);
537            getActivity().finish();
538        }
539    }
540}
541