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