KeyguardPatternView.java revision 3018197cf0dff5a9061f6065a8ecc108a0866dab
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.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.animation.Animator;
25import android.animation.AnimatorListenerAdapter;
26import android.animation.ValueAnimator;
27import android.content.Context;
28import android.graphics.Rect;
29import android.graphics.drawable.Drawable;
30import android.os.Bundle;
31import android.os.CountDownTimer;
32import android.os.SystemClock;
33import android.os.UserHandle;
34import android.text.TextUtils;
35import android.util.AttributeSet;
36import android.util.Log;
37import android.view.MotionEvent;
38import android.view.View;
39import android.view.ViewGroup;
40import android.view.animation.Interpolator;
41import android.widget.Button;
42import android.widget.LinearLayout;
43
44import com.android.internal.widget.LockPatternUtils;
45import com.android.internal.widget.LockPatternView;
46
47import java.io.IOException;
48import java.util.List;
49
50public class KeyguardPatternView extends LinearLayout implements KeyguardSecurityView,
51        AppearAnimationCreator<LockPatternView.CellState> {
52
53    private static final String TAG = "SecurityPatternView";
54    private static final boolean DEBUG = KeyguardConstants.DEBUG;
55
56    // how long before we clear the wrong pattern
57    private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000;
58
59    // how long we stay awake after each key beyond MIN_PATTERN_BEFORE_POKE_WAKELOCK
60    private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000;
61
62    // how long we stay awake after the user hits the first dot.
63    private static final int UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS = 2000;
64
65    // how many cells the user has to cross before we poke the wakelock
66    private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2;
67
68    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
69    private final AppearAnimationUtils mAppearAnimationUtils;
70
71    private CountDownTimer mCountdownTimer = null;
72    private LockPatternUtils mLockPatternUtils;
73    private LockPatternView mLockPatternView;
74    private Button mForgotPatternButton;
75    private KeyguardSecurityCallback mCallback;
76    private boolean mEnableFallback;
77
78    /**
79     * Keeps track of the last time we poked the wake lock during dispatching of the touch event.
80     * Initialized to something guaranteed to make us poke the wakelock when the user starts
81     * drawing the pattern.
82     * @see #dispatchTouchEvent(android.view.MotionEvent)
83     */
84    private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS;
85
86    /**
87     * Useful for clearing out the wrong pattern after a delay
88     */
89    private Runnable mCancelPatternRunnable = new Runnable() {
90        public void run() {
91            mLockPatternView.clearPattern();
92        }
93    };
94    private Rect mTempRect = new Rect();
95    private SecurityMessageDisplay mSecurityMessageDisplay;
96    private View mEcaView;
97    private Drawable mBouncerFrame;
98    private ViewGroup mKeyguardBouncerFrame;
99    private KeyguardMessageArea mHelpMessage;
100
101    enum FooterMode {
102        Normal,
103        ForgotLockPattern,
104        VerifyUnlocked
105    }
106
107    public KeyguardPatternView(Context context) {
108        this(context, null);
109    }
110
111    public KeyguardPatternView(Context context, AttributeSet attrs) {
112        super(context, attrs);
113        mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
114        mAppearAnimationUtils = new AppearAnimationUtils(context, 1.5f /* delayScale */,
115                2.0f /* transitionScale */);
116    }
117
118    public void setKeyguardCallback(KeyguardSecurityCallback callback) {
119        mCallback = callback;
120    }
121
122    public void setLockPatternUtils(LockPatternUtils utils) {
123        mLockPatternUtils = utils;
124    }
125
126    @Override
127    protected void onFinishInflate() {
128        super.onFinishInflate();
129        mLockPatternUtils = mLockPatternUtils == null
130                ? new LockPatternUtils(mContext) : mLockPatternUtils;
131
132        mLockPatternView = (LockPatternView) findViewById(R.id.lockPatternView);
133        mLockPatternView.setSaveEnabled(false);
134        mLockPatternView.setFocusable(false);
135        mLockPatternView.setOnPatternListener(new UnlockPatternListener());
136
137        // stealth mode will be the same for the life of this screen
138        mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled());
139
140        // vibrate mode will be the same for the life of this screen
141        mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled());
142
143        mForgotPatternButton = (Button) findViewById(R.id.forgot_password_button);
144        // note: some configurations don't have an emergency call area
145        if (mForgotPatternButton != null) {
146            mForgotPatternButton.setText(R.string.kg_forgot_pattern_button_text);
147            mForgotPatternButton.setOnClickListener(new OnClickListener() {
148                public void onClick(View v) {
149                    mCallback.showBackupSecurity();
150                }
151            });
152        }
153
154        setFocusableInTouchMode(true);
155
156        maybeEnableFallback(mContext);
157        mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this);
158        mEcaView = findViewById(R.id.keyguard_selector_fade_container);
159        View bouncerFrameView = findViewById(R.id.keyguard_bouncer_frame);
160        if (bouncerFrameView != null) {
161            mBouncerFrame = bouncerFrameView.getBackground();
162        }
163
164        mKeyguardBouncerFrame = (ViewGroup) findViewById(R.id.keyguard_bouncer_frame);
165        mHelpMessage = (KeyguardMessageArea) findViewById(R.id.keyguard_message_area);
166    }
167
168    private void updateFooter(FooterMode mode) {
169        if (mForgotPatternButton == null) return; // no ECA? no footer
170
171        switch (mode) {
172            case Normal:
173                if (DEBUG) Log.d(TAG, "mode normal");
174                mForgotPatternButton.setVisibility(View.GONE);
175                break;
176            case ForgotLockPattern:
177                if (DEBUG) Log.d(TAG, "mode ForgotLockPattern");
178                mForgotPatternButton.setVisibility(View.VISIBLE);
179                break;
180            case VerifyUnlocked:
181                if (DEBUG) Log.d(TAG, "mode VerifyUnlocked");
182                mForgotPatternButton.setVisibility(View.GONE);
183        }
184    }
185
186    @Override
187    public boolean onTouchEvent(MotionEvent ev) {
188        boolean result = super.onTouchEvent(ev);
189        // as long as the user is entering a pattern (i.e sending a touch event that was handled
190        // by this screen), keep poking the wake lock so that the screen will stay on.
191        final long elapsed = SystemClock.elapsedRealtime() - mLastPokeTime;
192        if (result && (elapsed > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) {
193            mLastPokeTime = SystemClock.elapsedRealtime();
194        }
195        mTempRect.set(0, 0, 0, 0);
196        offsetRectIntoDescendantCoords(mLockPatternView, mTempRect);
197        ev.offsetLocation(mTempRect.left, mTempRect.top);
198        result = mLockPatternView.dispatchTouchEvent(ev) || result;
199        ev.offsetLocation(-mTempRect.left, -mTempRect.top);
200        return result;
201    }
202
203    public void reset() {
204        // reset lock pattern
205        mLockPatternView.enableInput();
206        mLockPatternView.setEnabled(true);
207        mLockPatternView.clearPattern();
208
209        // if the user is currently locked out, enforce it.
210        long deadline = mLockPatternUtils.getLockoutAttemptDeadline();
211        if (deadline != 0) {
212            handleAttemptLockout(deadline);
213        } else {
214            displayDefaultSecurityMessage();
215        }
216
217        // the footer depends on how many total attempts the user has failed
218        if (mCallback.isVerifyUnlockOnly()) {
219            updateFooter(FooterMode.VerifyUnlocked);
220        } else if (mEnableFallback &&
221                (mKeyguardUpdateMonitor.getFailedUnlockAttempts()
222                        >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) {
223            updateFooter(FooterMode.ForgotLockPattern);
224        } else {
225            updateFooter(FooterMode.Normal);
226        }
227
228    }
229
230    private void displayDefaultSecurityMessage() {
231        if (mKeyguardUpdateMonitor.getMaxBiometricUnlockAttemptsReached()) {
232            mSecurityMessageDisplay.setMessage(R.string.faceunlock_multiple_failures, true);
233        } else {
234            mSecurityMessageDisplay.setMessage(R.string.kg_pattern_instructions, false);
235        }
236    }
237
238    @Override
239    public void showUsabilityHint() {
240    }
241
242    /** TODO: hook this up */
243    public void cleanUp() {
244        if (DEBUG) Log.v(TAG, "Cleanup() called on " + this);
245        mLockPatternUtils = null;
246        mLockPatternView.setOnPatternListener(null);
247    }
248
249    private class UnlockPatternListener implements LockPatternView.OnPatternListener {
250
251        public void onPatternStart() {
252            mLockPatternView.removeCallbacks(mCancelPatternRunnable);
253        }
254
255        public void onPatternCleared() {
256        }
257
258        public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
259            // To guard against accidental poking of the wakelock, look for
260            // the user actually trying to draw a pattern of some minimal length.
261            if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) {
262                mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_MS);
263            } else {
264                // Give just a little extra time if they hit one of the first few dots
265                mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS);
266            }
267        }
268
269        public void onPatternDetected(List<LockPatternView.Cell> pattern) {
270            if (mLockPatternUtils.checkPattern(pattern)) {
271                mCallback.reportUnlockAttempt(true);
272                mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct);
273                mCallback.dismiss(true);
274            } else {
275                if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) {
276                    mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_MS);
277                }
278                mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
279                boolean registeredAttempt =
280                        pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL;
281                if (registeredAttempt) {
282                    mCallback.reportUnlockAttempt(false);
283                }
284                int attempts = mKeyguardUpdateMonitor.getFailedUnlockAttempts();
285                if (registeredAttempt &&
286                        0 == (attempts % LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) {
287                    long deadline = mLockPatternUtils.setLockoutAttemptDeadline();
288                    handleAttemptLockout(deadline);
289                } else {
290                    mSecurityMessageDisplay.setMessage(R.string.kg_wrong_pattern, true);
291                    mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS);
292                }
293            }
294        }
295    }
296
297    private void maybeEnableFallback(Context context) {
298        // Ask the account manager if we have an account that can be used as a
299        // fallback in case the user forgets his pattern.
300        AccountAnalyzer accountAnalyzer = new AccountAnalyzer(AccountManager.get(context));
301        accountAnalyzer.start();
302    }
303
304    private class AccountAnalyzer implements AccountManagerCallback<Bundle> {
305        private final AccountManager mAccountManager;
306        private final Account[] mAccounts;
307        private int mAccountIndex;
308
309        private AccountAnalyzer(AccountManager accountManager) {
310            mAccountManager = accountManager;
311            mAccounts = accountManager.getAccountsByTypeAsUser("com.google",
312                    new UserHandle(mLockPatternUtils.getCurrentUser()));
313        }
314
315        private void next() {
316            // if we are ready to enable the fallback or if we depleted the list of accounts
317            // then finish and get out
318            if (mEnableFallback || mAccountIndex >= mAccounts.length) {
319                return;
320            }
321
322            // lookup the confirmCredentials intent for the current account
323            mAccountManager.confirmCredentialsAsUser(mAccounts[mAccountIndex], null, null, this,
324                    null, new UserHandle(mLockPatternUtils.getCurrentUser()));
325        }
326
327        public void start() {
328            mEnableFallback = false;
329            mAccountIndex = 0;
330            next();
331        }
332
333        public void run(AccountManagerFuture<Bundle> future) {
334            try {
335                Bundle result = future.getResult();
336                if (result.getParcelable(AccountManager.KEY_INTENT) != null) {
337                    mEnableFallback = true;
338                }
339            } catch (OperationCanceledException e) {
340                // just skip the account if we are unable to query it
341            } catch (IOException e) {
342                // just skip the account if we are unable to query it
343            } catch (AuthenticatorException e) {
344                // just skip the account if we are unable to query it
345            } finally {
346                mAccountIndex++;
347                next();
348            }
349        }
350    }
351
352    private void handleAttemptLockout(long elapsedRealtimeDeadline) {
353        mLockPatternView.clearPattern();
354        mLockPatternView.setEnabled(false);
355        final long elapsedRealtime = SystemClock.elapsedRealtime();
356        if (mEnableFallback) {
357            updateFooter(FooterMode.ForgotLockPattern);
358        }
359
360        mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) {
361
362            @Override
363            public void onTick(long millisUntilFinished) {
364                final int secondsRemaining = (int) (millisUntilFinished / 1000);
365                mSecurityMessageDisplay.setMessage(
366                        R.string.kg_too_many_failed_attempts_countdown, true, secondsRemaining);
367            }
368
369            @Override
370            public void onFinish() {
371                mLockPatternView.setEnabled(true);
372                displayDefaultSecurityMessage();
373                // TODO mUnlockIcon.setVisibility(View.VISIBLE);
374                if (mEnableFallback) {
375                    updateFooter(FooterMode.ForgotLockPattern);
376                } else {
377                    updateFooter(FooterMode.Normal);
378                }
379            }
380
381        }.start();
382    }
383
384    @Override
385    public boolean needsInput() {
386        return false;
387    }
388
389    @Override
390    public void onPause() {
391        if (mCountdownTimer != null) {
392            mCountdownTimer.cancel();
393            mCountdownTimer = null;
394        }
395    }
396
397    @Override
398    public void onResume(int reason) {
399        reset();
400    }
401
402    @Override
403    public KeyguardSecurityCallback getCallback() {
404        return mCallback;
405    }
406
407    @Override
408    public void showBouncer(int duration) {
409        KeyguardSecurityViewHelper.
410                showBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration);
411    }
412
413    @Override
414    public void hideBouncer(int duration) {
415        KeyguardSecurityViewHelper.
416                hideBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration);
417    }
418
419    @Override
420    public void startAppearAnimation() {
421        enableClipping(false);
422        mAppearAnimationUtils.startAppearAnimation(
423                mLockPatternView.getCellStates(),
424                new Runnable() {
425                    @Override
426                    public void run() {
427                        enableClipping(true);
428                    }
429                },
430                this);
431        if (!TextUtils.isEmpty(mHelpMessage.getText())) {
432            mAppearAnimationUtils.createAnimation(mHelpMessage, 0,
433                    AppearAnimationUtils.APPEAR_DURATION,
434                    mAppearAnimationUtils.getStartTranslation(),
435                    mAppearAnimationUtils.getInterpolator(),
436                    null /* finishRunnable */);
437        }
438    }
439
440    private void enableClipping(boolean enable) {
441        setClipChildren(enable);
442        mKeyguardBouncerFrame.setClipToPadding(enable);
443        mKeyguardBouncerFrame.setClipChildren(enable);
444    }
445
446    @Override
447    public void createAnimation(final LockPatternView.CellState animatedCell, long delay,
448            long duration, float startTranslationY, Interpolator interpolator,
449            final Runnable finishListener) {
450        animatedCell.scale = 0.0f;
451        animatedCell.translateY = startTranslationY;
452        ValueAnimator animator = ValueAnimator.ofFloat(startTranslationY, 0.0f);
453        animator.setInterpolator(interpolator);
454        animator.setDuration(duration);
455        animator.setStartDelay(delay);
456        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
457            @Override
458            public void onAnimationUpdate(ValueAnimator animation) {
459                float animatedFraction = animation.getAnimatedFraction();
460                animatedCell.scale = animatedFraction;
461                animatedCell.translateY = (float) animation.getAnimatedValue();
462                mLockPatternView.invalidate();
463            }
464        });
465        if (finishListener != null) {
466            animator.addListener(new AnimatorListenerAdapter() {
467                @Override
468                public void onAnimationEnd(Animator animation) {
469                    finishListener.run();
470                }
471            });
472
473            // Also animate the Emergency call
474            mAppearAnimationUtils.createAnimation(mEcaView, delay, duration, startTranslationY,
475            interpolator, null);
476
477            // And the forgot pattern button
478            if (mForgotPatternButton.getVisibility() == View.VISIBLE) {
479                mAppearAnimationUtils.createAnimation(mForgotPatternButton, delay, duration,
480                        startTranslationY, interpolator, null);
481            }
482        }
483        animator.start();
484        mLockPatternView.invalidate();
485    }
486}
487