KeyguardPatternView.java revision 6e38058908d6e49a241e384cd7023d9ac0927afb
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.animation.Animator;
19import android.animation.AnimatorListenerAdapter;
20import android.animation.ValueAnimator;
21import android.content.Context;
22import android.graphics.Rect;
23import android.os.AsyncTask;
24import android.os.CountDownTimer;
25import android.os.SystemClock;
26import android.text.TextUtils;
27import android.util.AttributeSet;
28import android.util.Log;
29import android.view.MotionEvent;
30import android.view.View;
31import android.view.ViewGroup;
32import android.view.animation.AnimationUtils;
33import android.view.animation.Interpolator;
34import android.widget.LinearLayout;
35
36import com.android.internal.widget.LockPatternChecker;
37import com.android.internal.widget.LockPatternUtils;
38import com.android.internal.widget.LockPatternView;
39
40import java.util.List;
41
42public class KeyguardPatternView extends LinearLayout implements KeyguardSecurityView,
43        AppearAnimationCreator<LockPatternView.CellState>,
44        EmergencyButton.EmergencyButtonCallback {
45
46    private static final String TAG = "SecurityPatternView";
47    private static final boolean DEBUG = KeyguardConstants.DEBUG;
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 many cells the user has to cross before we poke the wakelock
56    private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2;
57
58    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
59    private final AppearAnimationUtils mAppearAnimationUtils;
60    private final DisappearAnimationUtils mDisappearAnimationUtils;
61
62    private CountDownTimer mCountdownTimer = null;
63    private LockPatternUtils mLockPatternUtils;
64    private AsyncTask<?, ?, ?> mPendingLockCheck;
65    private LockPatternView mLockPatternView;
66    private KeyguardSecurityCallback mCallback;
67
68    /**
69     * Keeps track of the last time we poked the wake lock during dispatching of the touch event.
70     * Initialized to something guaranteed to make us poke the wakelock when the user starts
71     * drawing the pattern.
72     * @see #dispatchTouchEvent(android.view.MotionEvent)
73     */
74    private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS;
75
76    /**
77     * Useful for clearing out the wrong pattern after a delay
78     */
79    private Runnable mCancelPatternRunnable = new Runnable() {
80        public void run() {
81            mLockPatternView.clearPattern();
82        }
83    };
84    private Rect mTempRect = new Rect();
85    private SecurityMessageDisplay mSecurityMessageDisplay;
86    private View mEcaView;
87    private ViewGroup mContainer;
88    private KeyguardMessageArea mHelpMessage;
89    private int mDisappearYTranslation;
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        mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
104        mAppearAnimationUtils = new AppearAnimationUtils(context,
105                AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 1.5f /* translationScale */,
106                2.0f /* delayScale */, AnimationUtils.loadInterpolator(
107                        mContext, android.R.interpolator.linear_out_slow_in));
108        mDisappearAnimationUtils = new DisappearAnimationUtils(context,
109                125, 1.2f /* translationScale */,
110                0.8f /* delayScale */, AnimationUtils.loadInterpolator(
111                        mContext, android.R.interpolator.fast_out_linear_in));
112        mDisappearYTranslation = getResources().getDimensionPixelSize(
113                R.dimen.disappear_y_translation);
114    }
115
116    public void setKeyguardCallback(KeyguardSecurityCallback callback) {
117        mCallback = callback;
118    }
119
120    public void setLockPatternUtils(LockPatternUtils utils) {
121        mLockPatternUtils = utils;
122    }
123
124    @Override
125    protected void onFinishInflate() {
126        super.onFinishInflate();
127        mLockPatternUtils = mLockPatternUtils == null
128                ? new LockPatternUtils(mContext) : mLockPatternUtils;
129
130        mLockPatternView = (LockPatternView) findViewById(R.id.lockPatternView);
131        mLockPatternView.setSaveEnabled(false);
132        mLockPatternView.setFocusable(false);
133        mLockPatternView.setOnPatternListener(new UnlockPatternListener());
134
135        // stealth mode will be the same for the life of this screen
136        mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled(
137                KeyguardUpdateMonitor.getCurrentUser()));
138
139        // vibrate mode will be the same for the life of this screen
140        mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled());
141
142        setFocusableInTouchMode(true);
143
144        mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this);
145        mEcaView = findViewById(R.id.keyguard_selector_fade_container);
146        mContainer = (ViewGroup) findViewById(R.id.container);
147        mHelpMessage = (KeyguardMessageArea) findViewById(R.id.keyguard_message_area);
148
149        EmergencyButton button = (EmergencyButton) findViewById(R.id.emergency_call_button);
150        if (button != null) {
151            button.setCallback(this);
152        }
153    }
154
155    public void onEmergencyButtonClickedWhenInCall() {
156        mCallback.reset();
157    }
158
159    @Override
160    public boolean onTouchEvent(MotionEvent ev) {
161        boolean result = super.onTouchEvent(ev);
162        // as long as the user is entering a pattern (i.e sending a touch event that was handled
163        // by this screen), keep poking the wake lock so that the screen will stay on.
164        final long elapsed = SystemClock.elapsedRealtime() - mLastPokeTime;
165        if (result && (elapsed > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) {
166            mLastPokeTime = SystemClock.elapsedRealtime();
167        }
168        mTempRect.set(0, 0, 0, 0);
169        offsetRectIntoDescendantCoords(mLockPatternView, mTempRect);
170        ev.offsetLocation(mTempRect.left, mTempRect.top);
171        result = mLockPatternView.dispatchTouchEvent(ev) || result;
172        ev.offsetLocation(-mTempRect.left, -mTempRect.top);
173        return result;
174    }
175
176    public void reset() {
177        // reset lock pattern
178        mLockPatternView.enableInput();
179        mLockPatternView.setEnabled(true);
180        mLockPatternView.clearPattern();
181
182        // if the user is currently locked out, enforce it.
183        long deadline = mLockPatternUtils.getLockoutAttemptDeadline(
184                KeyguardUpdateMonitor.getCurrentUser());
185        if (deadline != 0) {
186            handleAttemptLockout(deadline);
187        } else {
188            displayDefaultSecurityMessage();
189        }
190    }
191
192    private void displayDefaultSecurityMessage() {
193        mSecurityMessageDisplay.setMessage(R.string.kg_pattern_instructions, false);
194    }
195
196    @Override
197    public void showUsabilityHint() {
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    private class UnlockPatternListener implements LockPatternView.OnPatternListener {
208
209        public void onPatternStart() {
210            mLockPatternView.removeCallbacks(mCancelPatternRunnable);
211        }
212
213        public void onPatternCleared() {
214        }
215
216        public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
217            mCallback.userActivity();
218        }
219
220        public void onPatternDetected(final List<LockPatternView.Cell> pattern) {
221            mLockPatternView.disableInput();
222            if (mPendingLockCheck != null) {
223                mPendingLockCheck.cancel(false);
224            }
225
226            mPendingLockCheck = LockPatternChecker.checkPattern(
227                    mLockPatternUtils,
228                    pattern,
229                    KeyguardUpdateMonitor.getCurrentUser(),
230                    new LockPatternChecker.OnCheckCallback() {
231                        @Override
232                        public void onChecked(boolean matched) {
233                            mLockPatternView.enableInput();
234                            mPendingLockCheck = null;
235                            onPatternChecked(pattern, matched);
236                        }
237                    });
238        }
239
240        private void onPatternChecked(List<LockPatternView.Cell> pattern, boolean matched) {
241            if (matched) {
242                mCallback.reportUnlockAttempt(true);
243                mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct);
244                mCallback.dismiss(true);
245            } else {
246                if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) {
247                    mCallback.userActivity();
248                }
249                mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
250                boolean registeredAttempt =
251                        pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL;
252                if (registeredAttempt) {
253                    mCallback.reportUnlockAttempt(false);
254                }
255                int attempts = mKeyguardUpdateMonitor.getFailedUnlockAttempts();
256                if (registeredAttempt &&
257                        0 == (attempts % LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) {
258                    long deadline = mLockPatternUtils.setLockoutAttemptDeadline(
259                            KeyguardUpdateMonitor.getCurrentUser());
260                    handleAttemptLockout(deadline);
261                } else {
262                    mSecurityMessageDisplay.setMessage(R.string.kg_wrong_pattern, true);
263                    mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS);
264                }
265            }
266        }
267    }
268
269    private void handleAttemptLockout(long elapsedRealtimeDeadline) {
270        mLockPatternView.clearPattern();
271        mLockPatternView.setEnabled(false);
272        final long elapsedRealtime = SystemClock.elapsedRealtime();
273
274        mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) {
275
276            @Override
277            public void onTick(long millisUntilFinished) {
278                final int secondsRemaining = (int) (millisUntilFinished / 1000);
279                mSecurityMessageDisplay.setMessage(
280                        R.string.kg_too_many_failed_attempts_countdown, true, secondsRemaining);
281            }
282
283            @Override
284            public void onFinish() {
285                mLockPatternView.setEnabled(true);
286                displayDefaultSecurityMessage();
287            }
288
289        }.start();
290    }
291
292    @Override
293    public boolean needsInput() {
294        return false;
295    }
296
297    @Override
298    public void onPause() {
299        if (mCountdownTimer != null) {
300            mCountdownTimer.cancel();
301            mCountdownTimer = null;
302        }
303        if (mPendingLockCheck != null) {
304            mPendingLockCheck.cancel(false);
305            mPendingLockCheck = null;
306        }
307    }
308
309    @Override
310    public void onResume(int reason) {
311        reset();
312    }
313
314    @Override
315    public KeyguardSecurityCallback getCallback() {
316        return mCallback;
317    }
318
319    @Override
320    public void startAppearAnimation() {
321        enableClipping(false);
322        setAlpha(1f);
323        setTranslationY(mAppearAnimationUtils.getStartTranslation());
324        animate()
325                .setDuration(500)
326                .setInterpolator(mAppearAnimationUtils.getInterpolator())
327                .translationY(0);
328        mAppearAnimationUtils.startAnimation(
329                mLockPatternView.getCellStates(),
330                new Runnable() {
331                    @Override
332                    public void run() {
333                        enableClipping(true);
334                    }
335                },
336                this);
337        if (!TextUtils.isEmpty(mHelpMessage.getText())) {
338            mAppearAnimationUtils.createAnimation(mHelpMessage, 0,
339                    AppearAnimationUtils.DEFAULT_APPEAR_DURATION,
340                    mAppearAnimationUtils.getStartTranslation(),
341                    true /* appearing */,
342                    mAppearAnimationUtils.getInterpolator(),
343                    null /* finishRunnable */);
344        }
345    }
346
347    @Override
348    public boolean startDisappearAnimation(final Runnable finishRunnable) {
349        mLockPatternView.clearPattern();
350        enableClipping(false);
351        setTranslationY(0);
352        animate()
353                .setDuration(300)
354                .setInterpolator(mDisappearAnimationUtils.getInterpolator())
355                .translationY(-mDisappearAnimationUtils.getStartTranslation());
356        mDisappearAnimationUtils.startAnimation(mLockPatternView.getCellStates(),
357                new Runnable() {
358                    @Override
359                    public void run() {
360                        enableClipping(true);
361                        if (finishRunnable != null) {
362                            finishRunnable.run();
363                        }
364                    }
365                }, KeyguardPatternView.this);
366        if (!TextUtils.isEmpty(mHelpMessage.getText())) {
367            mDisappearAnimationUtils.createAnimation(mHelpMessage, 0,
368                    200,
369                    - mDisappearAnimationUtils.getStartTranslation() * 3,
370                    false /* appearing */,
371                    mDisappearAnimationUtils.getInterpolator(),
372                    null /* finishRunnable */);
373        }
374        return true;
375    }
376
377    private void enableClipping(boolean enable) {
378        setClipChildren(enable);
379        mContainer.setClipToPadding(enable);
380        mContainer.setClipChildren(enable);
381    }
382
383    @Override
384    public void createAnimation(final LockPatternView.CellState animatedCell, long delay,
385            long duration, float translationY, final boolean appearing,
386            Interpolator interpolator,
387            final Runnable finishListener) {
388        if (appearing) {
389            animatedCell.scale = 0.0f;
390            animatedCell.alpha = 1.0f;
391        }
392        animatedCell.translateY = appearing ? translationY : 0;
393        ValueAnimator animator = ValueAnimator.ofFloat(animatedCell.translateY,
394                appearing ? 0 : translationY);
395        animator.setInterpolator(interpolator);
396        animator.setDuration(duration);
397        animator.setStartDelay(delay);
398        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
399            @Override
400            public void onAnimationUpdate(ValueAnimator animation) {
401                float animatedFraction = animation.getAnimatedFraction();
402                if (appearing) {
403                    animatedCell.scale = animatedFraction;
404                } else {
405                    animatedCell.alpha = 1 - animatedFraction;
406                }
407                animatedCell.translateY = (float) animation.getAnimatedValue();
408                mLockPatternView.invalidate();
409            }
410        });
411        if (finishListener != null) {
412            animator.addListener(new AnimatorListenerAdapter() {
413                @Override
414                public void onAnimationEnd(Animator animation) {
415                    finishListener.run();
416                }
417            });
418
419            // Also animate the Emergency call
420            mAppearAnimationUtils.createAnimation(mEcaView, delay, duration, translationY,
421                    appearing, interpolator, null);
422        }
423        animator.start();
424        mLockPatternView.invalidate();
425    }
426
427    @Override
428    public boolean hasOverlappingRendering() {
429        return false;
430    }
431}
432