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