1/*
2 * Copyright (C) 2008 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.android.internal.widget.LockPatternUtils;
20import com.android.internal.widget.LockPatternView;
21import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient;
22import com.android.internal.widget.LockPatternView.Cell;
23
24import android.app.Activity;
25import android.app.Fragment;
26import android.content.Intent;
27import android.os.CountDownTimer;
28import android.os.SystemClock;
29import android.os.Bundle;
30import android.preference.PreferenceActivity;
31import android.widget.TextView;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.view.ViewGroup;
35
36import java.util.List;
37
38/**
39 * Launch this when you want the user to confirm their lock pattern.
40 *
41 * Sets an activity result of {@link Activity#RESULT_OK} when the user
42 * successfully confirmed their pattern.
43 */
44public class ConfirmLockPattern extends PreferenceActivity {
45
46    /**
47     * Names of {@link CharSequence} fields within the originating {@link Intent}
48     * that are used to configure the keyguard confirmation view's labeling.
49     * The view will use the system-defined resource strings for any labels that
50     * the caller does not supply.
51     */
52    public static final String PACKAGE = "com.android.settings";
53    public static final String HEADER_TEXT = PACKAGE + ".ConfirmLockPattern.header";
54    public static final String FOOTER_TEXT = PACKAGE + ".ConfirmLockPattern.footer";
55    public static final String HEADER_WRONG_TEXT = PACKAGE + ".ConfirmLockPattern.header_wrong";
56    public static final String FOOTER_WRONG_TEXT = PACKAGE + ".ConfirmLockPattern.footer_wrong";
57
58    private enum Stage {
59        NeedToUnlock,
60        NeedToUnlockWrong,
61        LockedOut
62    }
63
64    @Override
65    public void onCreate(Bundle savedInstanceState) {
66        super.onCreate(savedInstanceState);
67        CharSequence msg = getText(R.string.lockpassword_confirm_your_pattern_header);
68        showBreadCrumbs(msg, msg);
69    }
70
71    @Override
72    public Intent getIntent() {
73        Intent modIntent = new Intent(super.getIntent());
74        modIntent.putExtra(EXTRA_SHOW_FRAGMENT, ConfirmLockPatternFragment.class.getName());
75        modIntent.putExtra(EXTRA_NO_HEADERS, true);
76        return modIntent;
77    }
78
79    @Override
80    protected boolean isValidFragment(String fragmentName) {
81        if (ConfirmLockPatternFragment.class.getName().equals(fragmentName)) return true;
82        return false;
83    }
84
85    public static class ConfirmLockPatternFragment extends Fragment {
86
87        // how long we wait to clear a wrong pattern
88        private static final int WRONG_PATTERN_CLEAR_TIMEOUT_MS = 2000;
89
90        private static final String KEY_NUM_WRONG_ATTEMPTS = "num_wrong_attempts";
91
92        private LockPatternView mLockPatternView;
93        private LockPatternUtils mLockPatternUtils;
94        private int mNumWrongConfirmAttempts;
95        private CountDownTimer mCountdownTimer;
96
97        private TextView mHeaderTextView;
98        private TextView mFooterTextView;
99
100        // caller-supplied text for various prompts
101        private CharSequence mHeaderText;
102        private CharSequence mFooterText;
103        private CharSequence mHeaderWrongText;
104        private CharSequence mFooterWrongText;
105
106        // required constructor for fragments
107        public ConfirmLockPatternFragment() {
108
109        }
110
111        @Override
112        public void onCreate(Bundle savedInstanceState) {
113            super.onCreate(savedInstanceState);
114            mLockPatternUtils = new LockPatternUtils(getActivity());
115        }
116
117        @Override
118        public View onCreateView(LayoutInflater inflater, ViewGroup container,
119                Bundle savedInstanceState) {
120            View view = inflater.inflate(R.layout.confirm_lock_pattern, null);
121            mHeaderTextView = (TextView) view.findViewById(R.id.headerText);
122            mLockPatternView = (LockPatternView) view.findViewById(R.id.lockPattern);
123            mFooterTextView = (TextView) view.findViewById(R.id.footerText);
124
125            // make it so unhandled touch events within the unlock screen go to the
126            // lock pattern view.
127            final LinearLayoutWithDefaultTouchRecepient topLayout
128                    = (LinearLayoutWithDefaultTouchRecepient) view.findViewById(R.id.topLayout);
129            topLayout.setDefaultTouchRecepient(mLockPatternView);
130
131            Intent intent = getActivity().getIntent();
132            if (intent != null) {
133                mHeaderText = intent.getCharSequenceExtra(HEADER_TEXT);
134                mFooterText = intent.getCharSequenceExtra(FOOTER_TEXT);
135                mHeaderWrongText = intent.getCharSequenceExtra(HEADER_WRONG_TEXT);
136                mFooterWrongText = intent.getCharSequenceExtra(FOOTER_WRONG_TEXT);
137            }
138
139            mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled());
140            mLockPatternView.setOnPatternListener(mConfirmExistingLockPatternListener);
141            updateStage(Stage.NeedToUnlock);
142
143            if (savedInstanceState != null) {
144                mNumWrongConfirmAttempts = savedInstanceState.getInt(KEY_NUM_WRONG_ATTEMPTS);
145            } else {
146                // on first launch, if no lock pattern is set, then finish with
147                // success (don't want user to get stuck confirming something that
148                // doesn't exist).
149                if (!mLockPatternUtils.savedPatternExists()) {
150                    getActivity().setResult(Activity.RESULT_OK);
151                    getActivity().finish();
152                }
153            }
154            return view;
155        }
156
157        @Override
158        public void onSaveInstanceState(Bundle outState) {
159            // deliberately not calling super since we are managing this in full
160            outState.putInt(KEY_NUM_WRONG_ATTEMPTS, mNumWrongConfirmAttempts);
161        }
162
163        @Override
164        public void onPause() {
165            super.onPause();
166
167            if (mCountdownTimer != null) {
168                mCountdownTimer.cancel();
169            }
170        }
171
172        @Override
173        public void onResume() {
174            super.onResume();
175
176            // if the user is currently locked out, enforce it.
177            long deadline = mLockPatternUtils.getLockoutAttemptDeadline();
178            if (deadline != 0) {
179                handleAttemptLockout(deadline);
180            } else if (!mLockPatternView.isEnabled()) {
181                // The deadline has passed, but the timer was cancelled...
182                // Need to clean up.
183                mNumWrongConfirmAttempts = 0;
184                updateStage(Stage.NeedToUnlock);
185            }
186        }
187
188        private void updateStage(Stage stage) {
189            switch (stage) {
190                case NeedToUnlock:
191                    if (mHeaderText != null) {
192                        mHeaderTextView.setText(mHeaderText);
193                    } else {
194                        mHeaderTextView.setText(R.string.lockpattern_need_to_unlock);
195                    }
196                    if (mFooterText != null) {
197                        mFooterTextView.setText(mFooterText);
198                    } else {
199                        mFooterTextView.setText(R.string.lockpattern_need_to_unlock_footer);
200                    }
201
202                    mLockPatternView.setEnabled(true);
203                    mLockPatternView.enableInput();
204                    break;
205                case NeedToUnlockWrong:
206                    if (mHeaderWrongText != null) {
207                        mHeaderTextView.setText(mHeaderWrongText);
208                    } else {
209                        mHeaderTextView.setText(R.string.lockpattern_need_to_unlock_wrong);
210                    }
211                    if (mFooterWrongText != null) {
212                        mFooterTextView.setText(mFooterWrongText);
213                    } else {
214                        mFooterTextView.setText(R.string.lockpattern_need_to_unlock_wrong_footer);
215                    }
216
217                    mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
218                    mLockPatternView.setEnabled(true);
219                    mLockPatternView.enableInput();
220                    break;
221                case LockedOut:
222                    mLockPatternView.clearPattern();
223                    // enabled = false means: disable input, and have the
224                    // appearance of being disabled.
225                    mLockPatternView.setEnabled(false); // appearance of being disabled
226                    break;
227            }
228
229            // Always announce the header for accessibility. This is a no-op
230            // when accessibility is disabled.
231            mHeaderTextView.announceForAccessibility(mHeaderTextView.getText());
232        }
233
234        private Runnable mClearPatternRunnable = new Runnable() {
235            public void run() {
236                mLockPatternView.clearPattern();
237            }
238        };
239
240        // clear the wrong pattern unless they have started a new one
241        // already
242        private void postClearPatternRunnable() {
243            mLockPatternView.removeCallbacks(mClearPatternRunnable);
244            mLockPatternView.postDelayed(mClearPatternRunnable, WRONG_PATTERN_CLEAR_TIMEOUT_MS);
245        }
246
247        /**
248         * The pattern listener that responds according to a user confirming
249         * an existing lock pattern.
250         */
251        private LockPatternView.OnPatternListener mConfirmExistingLockPatternListener
252                = new LockPatternView.OnPatternListener()  {
253
254            public void onPatternStart() {
255                mLockPatternView.removeCallbacks(mClearPatternRunnable);
256            }
257
258            public void onPatternCleared() {
259                mLockPatternView.removeCallbacks(mClearPatternRunnable);
260            }
261
262            public void onPatternCellAdded(List<Cell> pattern) {
263
264            }
265
266            public void onPatternDetected(List<LockPatternView.Cell> pattern) {
267                if (mLockPatternUtils.checkPattern(pattern)) {
268
269                    Intent intent = new Intent();
270                    intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD,
271                                    LockPatternUtils.patternToString(pattern));
272
273                    getActivity().setResult(Activity.RESULT_OK, intent);
274                    getActivity().finish();
275                } else {
276                    if (pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL &&
277                            ++mNumWrongConfirmAttempts
278                            >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) {
279                        long deadline = mLockPatternUtils.setLockoutAttemptDeadline();
280                        handleAttemptLockout(deadline);
281                    } else {
282                        updateStage(Stage.NeedToUnlockWrong);
283                        postClearPatternRunnable();
284                    }
285                }
286            }
287        };
288
289
290        private void handleAttemptLockout(long elapsedRealtimeDeadline) {
291            updateStage(Stage.LockedOut);
292            long elapsedRealtime = SystemClock.elapsedRealtime();
293            mCountdownTimer = new CountDownTimer(
294                    elapsedRealtimeDeadline - elapsedRealtime,
295                    LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS) {
296
297                @Override
298                public void onTick(long millisUntilFinished) {
299                    mHeaderTextView.setText(R.string.lockpattern_too_many_failed_confirmation_attempts_header);
300                    final int secondsCountdown = (int) (millisUntilFinished / 1000);
301                    mFooterTextView.setText(getString(
302                            R.string.lockpattern_too_many_failed_confirmation_attempts_footer,
303                            secondsCountdown));
304                }
305
306                @Override
307                public void onFinish() {
308                    mNumWrongConfirmAttempts = 0;
309                    updateStage(Stage.NeedToUnlock);
310                }
311            }.start();
312        }
313    }
314}
315