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