KeyguardPatternView.java revision 258341c377b6aa9f1bd29a9b507a97967e432dfe
1/*
2 * Copyright (C) 2012 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 */
16package com.android.internal.policy.impl.keyguard;
17
18import android.accounts.Account;
19import android.accounts.AccountManager;
20import android.accounts.AccountManagerCallback;
21import android.accounts.AccountManagerFuture;
22import android.accounts.AuthenticatorException;
23import android.accounts.OperationCanceledException;
24import android.content.Context;
25import android.os.Bundle;
26import android.os.CountDownTimer;
27import android.os.SystemClock;
28import android.security.KeyStore;
29import android.util.AttributeSet;
30import android.util.Log;
31import android.view.MotionEvent;
32import android.view.View;
33import android.widget.Button;
34import android.widget.LinearLayout;
35
36import com.android.internal.widget.LockPatternUtils;
37import com.android.internal.widget.LockPatternView;
38import com.android.internal.R;
39
40import java.io.IOException;
41import java.util.List;
42
43public class KeyguardPatternView extends LinearLayout implements KeyguardSecurityView {
44
45    private static final String TAG = "SecurityPatternView";
46    private static final boolean DEBUG = false;
47
48    // how long before we clear the wrong pattern
49    private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000;
50
51    // how long we stay awake after each key beyond MIN_PATTERN_BEFORE_POKE_WAKELOCK
52    private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000;
53
54    // how long we stay awake after the user hits the first dot.
55    private static final int UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS = 2000;
56
57    // how many cells the user has to cross before we poke the wakelock
58    private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2;
59
60    private int mFailedPatternAttemptsSinceLastTimeout = 0;
61    private int mTotalFailedPatternAttempts = 0;
62    private CountDownTimer mCountdownTimer = null;
63    private LockPatternUtils mLockPatternUtils;
64    private LockPatternView mLockPatternView;
65    private Button mForgotPatternButton;
66    private KeyguardSecurityCallback mCallback;
67    private boolean mEnableFallback;
68    private KeyguardNavigationManager mNavigationManager;
69
70    /**
71     * Keeps track of the last time we poked the wake lock during dispatching of the touch event.
72     * Initialized to something guaranteed to make us poke the wakelock when the user starts
73     * drawing the pattern.
74     * @see #dispatchTouchEvent(android.view.MotionEvent)
75     */
76    private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS;
77
78    /**
79     * Useful for clearing out the wrong pattern after a delay
80     */
81    private Runnable mCancelPatternRunnable = new Runnable() {
82        public void run() {
83            mLockPatternView.clearPattern();
84        }
85    };
86
87    enum FooterMode {
88        Normal,
89        ForgotLockPattern,
90        VerifyUnlocked
91    }
92
93    public KeyguardPatternView(Context context) {
94        this(context, null);
95    }
96
97    public KeyguardPatternView(Context context, AttributeSet attrs) {
98        super(context, attrs);
99    }
100
101    public void setKeyguardCallback(KeyguardSecurityCallback callback) {
102        mCallback = callback;
103    }
104
105    public void setLockPatternUtils(LockPatternUtils utils) {
106        mLockPatternUtils = utils;
107    }
108
109    @Override
110    protected void onFinishInflate() {
111        super.onFinishInflate();
112
113        mNavigationManager = new KeyguardNavigationManager(this);
114        mLockPatternUtils = mLockPatternUtils == null
115                ? new LockPatternUtils(mContext) : mLockPatternUtils;
116
117        mLockPatternView = (LockPatternView) findViewById(R.id.lockPatternView);
118        mLockPatternView.setSaveEnabled(false);
119        mLockPatternView.setFocusable(false);
120        mLockPatternView.setOnPatternListener(new UnlockPatternListener());
121
122        // stealth mode will be the same for the life of this screen
123        mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled());
124
125        // vibrate mode will be the same for the life of this screen
126        mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled());
127
128        mForgotPatternButton = (Button) findViewById(R.id.forgot_password_button);
129        mForgotPatternButton.setText(R.string.kg_forgot_pattern_button_text);
130        mForgotPatternButton.setOnClickListener(new OnClickListener() {
131            public void onClick(View v) {
132                mCallback.showBackupSecurity();
133            }
134        });
135
136        setFocusableInTouchMode(true);
137
138        maybeEnableFallback(mContext);
139    }
140
141    private void updateFooter(FooterMode mode) {
142        switch (mode) {
143            case Normal:
144                if (DEBUG) Log.d(TAG, "mode normal");
145                mForgotPatternButton.setVisibility(View.GONE);
146                break;
147            case ForgotLockPattern:
148                if (DEBUG) Log.d(TAG, "mode ForgotLockPattern");
149                mForgotPatternButton.setVisibility(View.VISIBLE);
150                break;
151            case VerifyUnlocked:
152                if (DEBUG) Log.d(TAG, "mode VerifyUnlocked");
153                mForgotPatternButton.setVisibility(View.GONE);
154        }
155    }
156
157    @Override
158    public boolean dispatchTouchEvent(MotionEvent ev) {
159        final boolean result = super.dispatchTouchEvent(ev);
160        // as long as the user is entering a pattern (i.e sending a touch event that was handled
161        // by this screen), keep poking the wake lock so that the screen will stay on.
162        final long elapsed = SystemClock.elapsedRealtime() - mLastPokeTime;
163        if (result && (elapsed > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) {
164            mLastPokeTime = SystemClock.elapsedRealtime();
165        }
166        return result;
167    }
168
169    public void reset() {
170        // reset lock pattern
171        mLockPatternView.enableInput();
172        mLockPatternView.setEnabled(true);
173        mLockPatternView.clearPattern();
174
175        // if the user is currently locked out, enforce it.
176        long deadline = mLockPatternUtils.getLockoutAttemptDeadline();
177        if (deadline != 0) {
178            handleAttemptLockout(deadline);
179        } else {
180            mNavigationManager.setMessage(R.string.kg_pattern_instructions);
181        }
182
183        // the footer depends on how many total attempts the user has failed
184        if (mCallback.isVerifyUnlockOnly()) {
185            updateFooter(FooterMode.VerifyUnlocked);
186        } else if (mEnableFallback &&
187                (mTotalFailedPatternAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) {
188            updateFooter(FooterMode.ForgotLockPattern);
189        } else {
190            updateFooter(FooterMode.Normal);
191        }
192
193    }
194
195    /** TODO: hook this up */
196    public void cleanUp() {
197        if (DEBUG) Log.v(TAG, "Cleanup() called on " + this);
198        mLockPatternUtils = null;
199        mLockPatternView.setOnPatternListener(null);
200    }
201
202    @Override
203    public void onWindowFocusChanged(boolean hasWindowFocus) {
204        super.onWindowFocusChanged(hasWindowFocus);
205        if (hasWindowFocus) {
206            // when timeout dialog closes we want to update our state
207            reset();
208        }
209    }
210
211    private class UnlockPatternListener implements LockPatternView.OnPatternListener {
212
213        public void onPatternStart() {
214            mLockPatternView.removeCallbacks(mCancelPatternRunnable);
215        }
216
217        public void onPatternCleared() {
218        }
219
220        public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
221            // To guard against accidental poking of the wakelock, look for
222            // the user actually trying to draw a pattern of some minimal length.
223            if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) {
224                mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_MS);
225            } else {
226                // Give just a little extra time if they hit one of the first few dots
227                mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS);
228            }
229        }
230
231        public void onPatternDetected(List<LockPatternView.Cell> pattern) {
232            if (mLockPatternUtils.checkPattern(pattern)) {
233                mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct);
234                mCallback.dismiss(true); // keyguardDone(true)
235                KeyStore.getInstance().password(LockPatternUtils.patternToString(pattern));
236                mTotalFailedPatternAttempts = 0;
237            } else {
238                if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) {
239                    mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_MS);
240                }
241                mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
242                if (pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
243                    mTotalFailedPatternAttempts++;
244                    mFailedPatternAttemptsSinceLastTimeout++;
245                    mCallback.reportFailedUnlockAttempt();
246                }
247                if (mFailedPatternAttemptsSinceLastTimeout
248                        >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) {
249                    long deadline = mLockPatternUtils.setLockoutAttemptDeadline();
250                    handleAttemptLockout(deadline);
251                } else {
252                    mNavigationManager.setMessage(R.string.kg_wrong_pattern);
253                    mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS);
254                }
255            }
256        }
257    }
258
259    private void maybeEnableFallback(Context context) {
260        // Ask the account manager if we have an account that can be used as a
261        // fallback in case the user forgets his pattern.
262        AccountAnalyzer accountAnalyzer = new AccountAnalyzer(AccountManager.get(context));
263        accountAnalyzer.start();
264    }
265
266    private class AccountAnalyzer implements AccountManagerCallback<Bundle> {
267        private final AccountManager mAccountManager;
268        private final Account[] mAccounts;
269        private int mAccountIndex;
270
271        private AccountAnalyzer(AccountManager accountManager) {
272            mAccountManager = accountManager;
273            mAccounts = accountManager.getAccountsByType("com.google");
274        }
275
276        private void next() {
277            // if we are ready to enable the fallback or if we depleted the list of accounts
278            // then finish and get out
279            if (mAccountIndex >= mAccounts.length) {
280                mEnableFallback = true;
281                return;
282            }
283
284            // lookup the confirmCredentials intent for the current account
285            mAccountManager.confirmCredentials(mAccounts[mAccountIndex], null, null, this, null);
286        }
287
288        public void start() {
289            mEnableFallback = false;
290            mAccountIndex = 0;
291            next();
292        }
293
294        public void run(AccountManagerFuture<Bundle> future) {
295            try {
296                Bundle result = future.getResult();
297                if (result.getParcelable(AccountManager.KEY_INTENT) != null) {
298                    mEnableFallback = true;
299                }
300            } catch (OperationCanceledException e) {
301                // just skip the account if we are unable to query it
302            } catch (IOException e) {
303                // just skip the account if we are unable to query it
304            } catch (AuthenticatorException e) {
305                // just skip the account if we are unable to query it
306            } finally {
307                mAccountIndex++;
308                next();
309            }
310        }
311    }
312
313    private void handleAttemptLockout(long elapsedRealtimeDeadline) {
314        mLockPatternView.clearPattern();
315        mLockPatternView.setEnabled(false);
316        final long elapsedRealtime = SystemClock.elapsedRealtime();
317        updateFooter(FooterMode.ForgotLockPattern);
318
319        mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) {
320
321            @Override
322            public void onTick(long millisUntilFinished) {
323                final int secondsRemaining = (int) (millisUntilFinished / 1000);
324                mNavigationManager.setMessage(
325                        R.string.kg_too_many_failed_attempts_countdown, secondsRemaining);
326            }
327
328            @Override
329            public void onFinish() {
330                mLockPatternView.setEnabled(true);
331                mNavigationManager.setMessage(R.string.kg_pattern_instructions);
332                // TODO mUnlockIcon.setVisibility(View.VISIBLE);
333                mFailedPatternAttemptsSinceLastTimeout = 0;
334                if (mEnableFallback) {
335                    updateFooter(FooterMode.ForgotLockPattern);
336                } else {
337                    updateFooter(FooterMode.Normal);
338                }
339            }
340
341        }.start();
342    }
343
344    @Override
345    public boolean needsInput() {
346        return false;
347    }
348
349    @Override
350    public void onPause() {
351        if (mCountdownTimer != null) {
352            mCountdownTimer.cancel();
353            mCountdownTimer = null;
354        }
355    }
356
357    @Override
358    public void onResume() {
359        reset();
360    }
361
362    @Override
363    public KeyguardSecurityCallback getCallback() {
364        return mCallback;
365    }
366
367}
368
369
370
371