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