KeyguardPatternView.java revision 5673353559453ecb57fc767b4e7500dd46e44079
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;
39import com.android.settingslib.animation.AppearAnimationCreator;
40import com.android.settingslib.animation.AppearAnimationUtils;
41import com.android.settingslib.animation.DisappearAnimationUtils;
42
43import java.util.List;
44
45public class KeyguardPatternView extends LinearLayout implements KeyguardSecurityView,
46        AppearAnimationCreator<LockPatternView.CellState>,
47        EmergencyButton.EmergencyButtonCallback {
48
49    private static final String TAG = "SecurityPatternView";
50    private static final boolean DEBUG = KeyguardConstants.DEBUG;
51
52    // how long before we clear the wrong pattern
53    private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000;
54
55    // how long we stay awake after each key beyond MIN_PATTERN_BEFORE_POKE_WAKELOCK
56    private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000;
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 final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
62    private final AppearAnimationUtils mAppearAnimationUtils;
63    private final DisappearAnimationUtils mDisappearAnimationUtils;
64
65    private CountDownTimer mCountdownTimer = null;
66    private LockPatternUtils mLockPatternUtils;
67    private AsyncTask<?, ?, ?> mPendingLockCheck;
68    private LockPatternView mLockPatternView;
69    private KeyguardSecurityCallback mCallback;
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 ViewGroup mContainer;
91    private KeyguardMessageArea mHelpMessage;
92    private int mDisappearYTranslation;
93
94    enum FooterMode {
95        Normal,
96        ForgotLockPattern,
97        VerifyUnlocked
98    }
99
100    public KeyguardPatternView(Context context) {
101        this(context, null);
102    }
103
104    public KeyguardPatternView(Context context, AttributeSet attrs) {
105        super(context, attrs);
106        mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
107        mAppearAnimationUtils = new AppearAnimationUtils(context,
108                AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 1.5f /* translationScale */,
109                2.0f /* delayScale */, AnimationUtils.loadInterpolator(
110                        mContext, android.R.interpolator.linear_out_slow_in));
111        mDisappearAnimationUtils = new DisappearAnimationUtils(context,
112                125, 1.2f /* translationScale */,
113                0.8f /* delayScale */, AnimationUtils.loadInterpolator(
114                        mContext, android.R.interpolator.fast_out_linear_in));
115        mDisappearYTranslation = getResources().getDimensionPixelSize(
116                R.dimen.disappear_y_translation);
117    }
118
119    public void setKeyguardCallback(KeyguardSecurityCallback callback) {
120        mCallback = callback;
121    }
122
123    public void setLockPatternUtils(LockPatternUtils utils) {
124        mLockPatternUtils = utils;
125    }
126
127    @Override
128    protected void onFinishInflate() {
129        super.onFinishInflate();
130        mLockPatternUtils = mLockPatternUtils == null
131                ? new LockPatternUtils(mContext) : mLockPatternUtils;
132
133        mLockPatternView = (LockPatternView) findViewById(R.id.lockPatternView);
134        mLockPatternView.setSaveEnabled(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                KeyguardUpdateMonitor.getCurrentUser()));
140
141        // vibrate mode will be the same for the life of this screen
142        mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled());
143
144        mSecurityMessageDisplay = KeyguardMessageArea.findSecurityMessageDisplay(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            mSecurityMessageDisplay.setMessage("", false);
212        }
213
214        public void onPatternCleared() {
215        }
216
217        public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
218            mCallback.userActivity();
219        }
220
221        public void onPatternDetected(final List<LockPatternView.Cell> pattern) {
222            mLockPatternView.disableInput();
223            if (mPendingLockCheck != null) {
224                mPendingLockCheck.cancel(false);
225            }
226
227            if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
228                mLockPatternView.enableInput();
229                onPatternChecked(pattern, false, 0);
230                return;
231            }
232
233            mPendingLockCheck = LockPatternChecker.checkPattern(
234                    mLockPatternUtils,
235                    pattern,
236                    KeyguardUpdateMonitor.getCurrentUser(),
237                    new LockPatternChecker.OnCheckCallback() {
238                        @Override
239                        public void onChecked(boolean matched, int timeoutMs) {
240                            mLockPatternView.enableInput();
241                            mPendingLockCheck = null;
242                            onPatternChecked(pattern, matched, timeoutMs);
243                        }
244                    });
245        }
246
247        private void onPatternChecked(List<LockPatternView.Cell> pattern, boolean matched,
248                int timeoutMs) {
249            if (matched) {
250                mCallback.reportUnlockAttempt(true, 0);
251                mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct);
252                mCallback.dismiss(true);
253            } else {
254                if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) {
255                    mCallback.userActivity();
256                }
257                mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
258                mCallback.reportUnlockAttempt(false, timeoutMs);
259                int attempts = mKeyguardUpdateMonitor.getFailedUnlockAttempts();
260                if (timeoutMs > 0) {
261                    long deadline = mLockPatternUtils.setLockoutAttemptDeadline(
262                            KeyguardUpdateMonitor.getCurrentUser(), timeoutMs);
263                    handleAttemptLockout(deadline);
264                } else {
265                    mSecurityMessageDisplay.setMessage(R.string.kg_wrong_pattern, true);
266                    mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS);
267                }
268            }
269        }
270    }
271
272    private void handleAttemptLockout(long elapsedRealtimeDeadline) {
273        mLockPatternView.clearPattern();
274        mLockPatternView.setEnabled(false);
275        final long elapsedRealtime = SystemClock.elapsedRealtime();
276
277        mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) {
278
279            @Override
280            public void onTick(long millisUntilFinished) {
281                final int secondsRemaining = (int) (millisUntilFinished / 1000);
282                mSecurityMessageDisplay.setMessage(
283                        R.string.kg_too_many_failed_attempts_countdown, true, secondsRemaining);
284            }
285
286            @Override
287            public void onFinish() {
288                mLockPatternView.setEnabled(true);
289                displayDefaultSecurityMessage();
290            }
291
292        }.start();
293    }
294
295    @Override
296    public boolean needsInput() {
297        return false;
298    }
299
300    @Override
301    public void onPause() {
302        if (mCountdownTimer != null) {
303            mCountdownTimer.cancel();
304            mCountdownTimer = null;
305        }
306        if (mPendingLockCheck != null) {
307            mPendingLockCheck.cancel(false);
308            mPendingLockCheck = null;
309        }
310    }
311
312    @Override
313    public void onResume(int reason) {
314        reset();
315    }
316
317    @Override
318    public KeyguardSecurityCallback getCallback() {
319        return mCallback;
320    }
321
322    @Override
323    public void startAppearAnimation() {
324        enableClipping(false);
325        setAlpha(1f);
326        setTranslationY(mAppearAnimationUtils.getStartTranslation());
327        animate()
328                .setDuration(500)
329                .setInterpolator(mAppearAnimationUtils.getInterpolator())
330                .translationY(0);
331        mAppearAnimationUtils.startAnimation2d(
332                mLockPatternView.getCellStates(),
333                new Runnable() {
334                    @Override
335                    public void run() {
336                        enableClipping(true);
337                    }
338                },
339                this);
340        if (!TextUtils.isEmpty(mHelpMessage.getText())) {
341            mAppearAnimationUtils.createAnimation(mHelpMessage, 0,
342                    AppearAnimationUtils.DEFAULT_APPEAR_DURATION,
343                    mAppearAnimationUtils.getStartTranslation(),
344                    true /* appearing */,
345                    mAppearAnimationUtils.getInterpolator(),
346                    null /* finishRunnable */);
347        }
348    }
349
350    @Override
351    public boolean startDisappearAnimation(final Runnable finishRunnable) {
352        mLockPatternView.clearPattern();
353        enableClipping(false);
354        setTranslationY(0);
355        animate()
356                .setDuration(300)
357                .setInterpolator(mDisappearAnimationUtils.getInterpolator())
358                .translationY(-mDisappearAnimationUtils.getStartTranslation());
359        mDisappearAnimationUtils.startAnimation2d(mLockPatternView.getCellStates(),
360                new Runnable() {
361                    @Override
362                    public void run() {
363                        enableClipping(true);
364                        if (finishRunnable != null) {
365                            finishRunnable.run();
366                        }
367                    }
368                }, KeyguardPatternView.this);
369        if (!TextUtils.isEmpty(mHelpMessage.getText())) {
370            mDisappearAnimationUtils.createAnimation(mHelpMessage, 0,
371                    200,
372                    - mDisappearAnimationUtils.getStartTranslation() * 3,
373                    false /* appearing */,
374                    mDisappearAnimationUtils.getInterpolator(),
375                    null /* finishRunnable */);
376        }
377        return true;
378    }
379
380    private void enableClipping(boolean enable) {
381        setClipChildren(enable);
382        mContainer.setClipToPadding(enable);
383        mContainer.setClipChildren(enable);
384    }
385
386    @Override
387    public void createAnimation(final LockPatternView.CellState animatedCell, long delay,
388            long duration, float translationY, final boolean appearing,
389            Interpolator interpolator,
390            final Runnable finishListener) {
391        if (appearing) {
392            animatedCell.scale = 0.0f;
393            animatedCell.alpha = 1.0f;
394        }
395        animatedCell.translateY = appearing ? translationY : 0;
396        ValueAnimator animator = ValueAnimator.ofFloat(animatedCell.translateY,
397                appearing ? 0 : translationY);
398        animator.setInterpolator(interpolator);
399        animator.setDuration(duration);
400        animator.setStartDelay(delay);
401        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
402            @Override
403            public void onAnimationUpdate(ValueAnimator animation) {
404                float animatedFraction = animation.getAnimatedFraction();
405                if (appearing) {
406                    animatedCell.scale = animatedFraction;
407                } else {
408                    animatedCell.alpha = 1 - animatedFraction;
409                }
410                animatedCell.translateY = (float) animation.getAnimatedValue();
411                mLockPatternView.invalidate();
412            }
413        });
414        if (finishListener != null) {
415            animator.addListener(new AnimatorListenerAdapter() {
416                @Override
417                public void onAnimationEnd(Animator animation) {
418                    finishListener.run();
419                }
420            });
421
422            // Also animate the Emergency call
423            mAppearAnimationUtils.createAnimation(mEcaView, delay, duration, translationY,
424                    appearing, interpolator, null);
425        }
426        animator.start();
427        mLockPatternView.invalidate();
428    }
429
430    @Override
431    public boolean hasOverlappingRendering() {
432        return false;
433    }
434}
435