KeyguardPatternView.java revision 0ff7f010f8bfd011f0915031b02739ae3bee401e
1bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson/*
2bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson * Copyright (C) 2012 The Android Open Source Project
3bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson *
4bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson * Licensed under the Apache License, Version 2.0 (the "License");
5bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson * you may not use this file except in compliance with the License.
6bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson * You may obtain a copy of the License at
7bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson *
8bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson *      http://www.apache.org/licenses/LICENSE-2.0
9bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson *
10bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson * Unless required by applicable law or agreed to in writing, software
11bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson * distributed under the License is distributed on an "AS IS" BASIS,
12bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson * See the License for the specific language governing permissions and
14bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson * limitations under the License.
15bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson */
16bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonpackage com.android.internal.policy.impl.keyguard;
17bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson
18bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport android.accounts.Account;
19bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport android.accounts.AccountManager;
20bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport android.accounts.AccountManagerCallback;
21bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport android.accounts.AccountManagerFuture;
22bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport android.accounts.AuthenticatorException;
23bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport android.accounts.OperationCanceledException;
24bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport android.content.Context;
25bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport android.graphics.Rect;
26bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport android.os.Bundle;
27bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport android.os.CountDownTimer;
28bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport android.os.SystemClock;
29bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport android.util.AttributeSet;
30bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport android.util.Log;
31bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport android.view.MotionEvent;
32bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport android.view.View;
33bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport android.widget.Button;
34bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport android.widget.LinearLayout;
35bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson
36bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport com.android.internal.widget.LockPatternUtils;
37bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport com.android.internal.widget.LockPatternView;
38bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport com.android.internal.R;
39bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson
40bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport java.io.IOException;
41bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonimport java.util.List;
42bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson
43bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilsonpublic class KeyguardPatternView extends LinearLayout implements KeyguardSecurityView {
44bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson
45bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson    private static final String TAG = "SecurityPatternView";
46bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson    private static final boolean DEBUG = false;
47bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson
48bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson    // how long before we clear the wrong pattern
49bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson    private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000;
50bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson
51bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson    // how long we stay awake after each key beyond MIN_PATTERN_BEFORE_POKE_WAKELOCK
52bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson    private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000;
53bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson
54bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson    // how long we stay awake after the user hits the first dot.
55bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson    private static final int UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS = 2000;
56bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson
57bd3dba4346223593ac6033a3d2a7d8ec6f20738bJesse Wilson    // 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        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 onTouchEvent(MotionEvent ev) {
159        boolean result = super.onTouchEvent(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        mTempRect.set(0, 0, 0, 0);
167        offsetRectIntoDescendantCoords(mLockPatternView, mTempRect);
168        ev.offsetLocation(mTempRect.left, mTempRect.top);
169        result = mLockPatternView.dispatchTouchEvent(ev) || result;
170        ev.offsetLocation(-mTempRect.left, -mTempRect.top);
171        return result;
172    }
173
174    public void reset() {
175        // reset lock pattern
176        mLockPatternView.enableInput();
177        mLockPatternView.setEnabled(true);
178        mLockPatternView.clearPattern();
179
180        // if the user is currently locked out, enforce it.
181        long deadline = mLockPatternUtils.getLockoutAttemptDeadline();
182        if (deadline != 0) {
183            handleAttemptLockout(deadline);
184        } else {
185            mSecurityMessageDisplay.setMessage(R.string.kg_pattern_instructions);
186        }
187
188        // the footer depends on how many total attempts the user has failed
189        if (mCallback.isVerifyUnlockOnly()) {
190            updateFooter(FooterMode.VerifyUnlocked);
191        } else if (mEnableFallback &&
192                (mTotalFailedPatternAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) {
193            updateFooter(FooterMode.ForgotLockPattern);
194        } else {
195            updateFooter(FooterMode.Normal);
196        }
197
198    }
199
200    /** TODO: hook this up */
201    public void cleanUp() {
202        if (DEBUG) Log.v(TAG, "Cleanup() called on " + this);
203        mLockPatternUtils = null;
204        mLockPatternView.setOnPatternListener(null);
205    }
206
207    @Override
208    public void onWindowFocusChanged(boolean hasWindowFocus) {
209        super.onWindowFocusChanged(hasWindowFocus);
210        if (hasWindowFocus) {
211            // when timeout dialog closes we want to update our state
212            reset();
213        }
214    }
215
216    private class UnlockPatternListener implements LockPatternView.OnPatternListener {
217
218        public void onPatternStart() {
219            mLockPatternView.removeCallbacks(mCancelPatternRunnable);
220        }
221
222        public void onPatternCleared() {
223        }
224
225        public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
226            // To guard against accidental poking of the wakelock, look for
227            // the user actually trying to draw a pattern of some minimal length.
228            if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) {
229                mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_MS);
230            } else {
231                // Give just a little extra time if they hit one of the first few dots
232                mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS);
233            }
234        }
235
236        public void onPatternDetected(List<LockPatternView.Cell> pattern) {
237            if (mLockPatternUtils.checkPattern(pattern)) {
238                mCallback.reportSuccessfulUnlockAttempt();
239                mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct);
240                mTotalFailedPatternAttempts = 0;
241                mCallback.dismiss(true);
242            } else {
243                if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) {
244                    mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_MS);
245                }
246                mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
247                if (pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
248                    mTotalFailedPatternAttempts++;
249                    mFailedPatternAttemptsSinceLastTimeout++;
250                    mCallback.reportFailedUnlockAttempt();
251                }
252                if (mFailedPatternAttemptsSinceLastTimeout
253                        >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) {
254                    long deadline = mLockPatternUtils.setLockoutAttemptDeadline();
255                    handleAttemptLockout(deadline);
256                } else {
257                    mSecurityMessageDisplay.setMessage(R.string.kg_wrong_pattern);
258                    mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS);
259                }
260            }
261        }
262    }
263
264    private void maybeEnableFallback(Context context) {
265        // Ask the account manager if we have an account that can be used as a
266        // fallback in case the user forgets his pattern.
267        AccountAnalyzer accountAnalyzer = new AccountAnalyzer(AccountManager.get(context));
268        accountAnalyzer.start();
269    }
270
271    private class AccountAnalyzer implements AccountManagerCallback<Bundle> {
272        private final AccountManager mAccountManager;
273        private final Account[] mAccounts;
274        private int mAccountIndex;
275
276        private AccountAnalyzer(AccountManager accountManager) {
277            mAccountManager = accountManager;
278            mAccounts = accountManager.getAccountsByType("com.google");
279        }
280
281        private void next() {
282            // if we are ready to enable the fallback or if we depleted the list of accounts
283            // then finish and get out
284            if (mAccountIndex >= mAccounts.length) {
285                mEnableFallback = true;
286                return;
287            }
288
289            // lookup the confirmCredentials intent for the current account
290            mAccountManager.confirmCredentials(mAccounts[mAccountIndex], null, null, this, null);
291        }
292
293        public void start() {
294            mEnableFallback = false;
295            mAccountIndex = 0;
296            next();
297        }
298
299        public void run(AccountManagerFuture<Bundle> future) {
300            try {
301                Bundle result = future.getResult();
302                if (result.getParcelable(AccountManager.KEY_INTENT) != null) {
303                    mEnableFallback = true;
304                }
305            } catch (OperationCanceledException e) {
306                // just skip the account if we are unable to query it
307            } catch (IOException e) {
308                // just skip the account if we are unable to query it
309            } catch (AuthenticatorException e) {
310                // just skip the account if we are unable to query it
311            } finally {
312                mAccountIndex++;
313                next();
314            }
315        }
316    }
317
318    private void handleAttemptLockout(long elapsedRealtimeDeadline) {
319        mLockPatternView.clearPattern();
320        mLockPatternView.setEnabled(false);
321        final long elapsedRealtime = SystemClock.elapsedRealtime();
322        updateFooter(FooterMode.ForgotLockPattern);
323
324        mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) {
325
326            @Override
327            public void onTick(long millisUntilFinished) {
328                final int secondsRemaining = (int) (millisUntilFinished / 1000);
329                mSecurityMessageDisplay.setMessage(
330                        R.string.kg_too_many_failed_attempts_countdown, secondsRemaining);
331            }
332
333            @Override
334            public void onFinish() {
335                mLockPatternView.setEnabled(true);
336                mSecurityMessageDisplay.setMessage(R.string.kg_pattern_instructions);
337                // TODO mUnlockIcon.setVisibility(View.VISIBLE);
338                mFailedPatternAttemptsSinceLastTimeout = 0;
339                if (mEnableFallback) {
340                    updateFooter(FooterMode.ForgotLockPattern);
341                } else {
342                    updateFooter(FooterMode.Normal);
343                }
344            }
345
346        }.start();
347    }
348
349    @Override
350    public boolean needsInput() {
351        return false;
352    }
353
354    @Override
355    public void onPause() {
356        if (mCountdownTimer != null) {
357            mCountdownTimer.cancel();
358            mCountdownTimer = null;
359        }
360    }
361
362    @Override
363    public void onResume() {
364        reset();
365    }
366
367    @Override
368    public KeyguardSecurityCallback getCallback() {
369        return mCallback;
370    }
371
372    @Override
373    public void setSecurityMessageDisplay(SecurityMessageDisplay display) {
374        mSecurityMessageDisplay = display;
375    }
376}
377
378
379
380