KeyguardPatternView.java revision 51efddbd3bb304de2dd47fa8cd1114ac555958bb
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 static com.android.keyguard.LatencyTracker.ACTION_CHECK_CREDENTIAL;
19import static com.android.keyguard.LatencyTracker.ACTION_CHECK_CREDENTIAL_UNLOCKED;
20
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    // How much we scale up the duration of the disappear animation when the current user is locked
62    public static final float DISAPPEAR_MULTIPLIER_LOCKED = 1.5f;
63
64    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
65    private final AppearAnimationUtils mAppearAnimationUtils;
66    private final DisappearAnimationUtils mDisappearAnimationUtils;
67    private final DisappearAnimationUtils mDisappearAnimationUtilsLocked;
68
69    private CountDownTimer mCountdownTimer = null;
70    private LockPatternUtils mLockPatternUtils;
71    private AsyncTask<?, ?, ?> mPendingLockCheck;
72    private LockPatternView mLockPatternView;
73    private KeyguardSecurityCallback mCallback;
74
75    /**
76     * Keeps track of the last time we poked the wake lock during dispatching of the touch event.
77     * Initialized to something guaranteed to make us poke the wakelock when the user starts
78     * drawing the pattern.
79     * @see #dispatchTouchEvent(android.view.MotionEvent)
80     */
81    private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS;
82
83    /**
84     * Useful for clearing out the wrong pattern after a delay
85     */
86    private Runnable mCancelPatternRunnable = new Runnable() {
87        @Override
88        public void run() {
89            mLockPatternView.clearPattern();
90        }
91    };
92    private Rect mTempRect = new Rect();
93    private KeyguardMessageArea mSecurityMessageDisplay;
94    private View mEcaView;
95    private ViewGroup mContainer;
96    private int mDisappearYTranslation;
97
98    enum FooterMode {
99        Normal,
100        ForgotLockPattern,
101        VerifyUnlocked
102    }
103
104    public KeyguardPatternView(Context context) {
105        this(context, null);
106    }
107
108    public KeyguardPatternView(Context context, AttributeSet attrs) {
109        super(context, attrs);
110        mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
111        mAppearAnimationUtils = new AppearAnimationUtils(context,
112                AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 1.5f /* translationScale */,
113                2.0f /* delayScale */, AnimationUtils.loadInterpolator(
114                        mContext, android.R.interpolator.linear_out_slow_in));
115        mDisappearAnimationUtils = new DisappearAnimationUtils(context,
116                125, 1.2f /* translationScale */,
117                0.6f /* delayScale */, AnimationUtils.loadInterpolator(
118                        mContext, android.R.interpolator.fast_out_linear_in));
119        mDisappearAnimationUtilsLocked = new DisappearAnimationUtils(context,
120                (long) (125 * DISAPPEAR_MULTIPLIER_LOCKED), 1.2f /* translationScale */,
121                0.6f /* delayScale */, AnimationUtils.loadInterpolator(
122                mContext, android.R.interpolator.fast_out_linear_in));
123        mDisappearYTranslation = getResources().getDimensionPixelSize(
124                R.dimen.disappear_y_translation);
125    }
126
127    @Override
128    public void setKeyguardCallback(KeyguardSecurityCallback callback) {
129        mCallback = callback;
130    }
131
132    @Override
133    public void setLockPatternUtils(LockPatternUtils utils) {
134        mLockPatternUtils = utils;
135    }
136
137    @Override
138    protected void onFinishInflate() {
139        super.onFinishInflate();
140        mLockPatternUtils = mLockPatternUtils == null
141                ? new LockPatternUtils(mContext) : mLockPatternUtils;
142
143        mLockPatternView = findViewById(R.id.lockPatternView);
144        mLockPatternView.setSaveEnabled(false);
145        mLockPatternView.setOnPatternListener(new UnlockPatternListener());
146
147        // vibrate mode will be the same for the life of this screen
148        mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled());
149
150        mSecurityMessageDisplay =
151                (KeyguardMessageArea) KeyguardMessageArea.findSecurityMessageDisplay(this);
152        mEcaView = findViewById(R.id.keyguard_selector_fade_container);
153        mContainer = findViewById(R.id.container);
154
155        EmergencyButton button = findViewById(R.id.emergency_call_button);
156        if (button != null) {
157            button.setCallback(this);
158        }
159    }
160
161    @Override
162    public void onEmergencyButtonClickedWhenInCall() {
163        mCallback.reset();
164    }
165
166    @Override
167    public boolean onTouchEvent(MotionEvent ev) {
168        boolean result = super.onTouchEvent(ev);
169        // as long as the user is entering a pattern (i.e sending a touch event that was handled
170        // by this screen), keep poking the wake lock so that the screen will stay on.
171        final long elapsed = SystemClock.elapsedRealtime() - mLastPokeTime;
172        if (result && (elapsed > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) {
173            mLastPokeTime = SystemClock.elapsedRealtime();
174        }
175        mTempRect.set(0, 0, 0, 0);
176        offsetRectIntoDescendantCoords(mLockPatternView, mTempRect);
177        ev.offsetLocation(mTempRect.left, mTempRect.top);
178        result = mLockPatternView.dispatchTouchEvent(ev) || result;
179        ev.offsetLocation(-mTempRect.left, -mTempRect.top);
180        return result;
181    }
182
183    @Override
184    public void reset() {
185        // reset lock pattern
186        mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled(
187                KeyguardUpdateMonitor.getCurrentUser()));
188        mLockPatternView.enableInput();
189        mLockPatternView.setEnabled(true);
190        mLockPatternView.clearPattern();
191
192        // if the user is currently locked out, enforce it.
193        long deadline = mLockPatternUtils.getLockoutAttemptDeadline(
194                KeyguardUpdateMonitor.getCurrentUser());
195        if (deadline != 0) {
196            handleAttemptLockout(deadline);
197        } else {
198            displayDefaultSecurityMessage();
199        }
200    }
201
202    private void displayDefaultSecurityMessage() {
203        mSecurityMessageDisplay.setMessage("");
204    }
205
206    @Override
207    public void showUsabilityHint() {
208    }
209
210    /** TODO: hook this up */
211    public void cleanUp() {
212        if (DEBUG) Log.v(TAG, "Cleanup() called on " + this);
213        mLockPatternUtils = null;
214        mLockPatternView.setOnPatternListener(null);
215    }
216
217    private class UnlockPatternListener implements LockPatternView.OnPatternListener {
218
219        @Override
220        public void onPatternStart() {
221            mLockPatternView.removeCallbacks(mCancelPatternRunnable);
222            mSecurityMessageDisplay.setMessage("");
223        }
224
225        @Override
226        public void onPatternCleared() {
227        }
228
229        @Override
230        public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
231            mCallback.userActivity();
232        }
233
234        @Override
235        public void onPatternDetected(final List<LockPatternView.Cell> pattern) {
236            mLockPatternView.disableInput();
237            if (mPendingLockCheck != null) {
238                mPendingLockCheck.cancel(false);
239            }
240
241            final int userId = KeyguardUpdateMonitor.getCurrentUser();
242            if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
243                mLockPatternView.enableInput();
244                onPatternChecked(userId, false, 0, false /* not valid - too short */);
245                return;
246            }
247
248            if (LatencyTracker.isEnabled(mContext)) {
249                LatencyTracker.getInstance(mContext).onActionStart(ACTION_CHECK_CREDENTIAL);
250                LatencyTracker.getInstance(mContext).onActionStart(ACTION_CHECK_CREDENTIAL_UNLOCKED);
251            }
252            mPendingLockCheck = LockPatternChecker.checkPattern(
253                    mLockPatternUtils,
254                    pattern,
255                    userId,
256                    new LockPatternChecker.OnCheckCallback() {
257
258                        @Override
259                        public void onEarlyMatched() {
260                            if (LatencyTracker.isEnabled(mContext)) {
261                                LatencyTracker.getInstance(mContext).onActionEnd(
262                                        ACTION_CHECK_CREDENTIAL);
263                            }
264                            onPatternChecked(userId, true /* matched */, 0 /* timeoutMs */,
265                                    true /* isValidPattern */);
266                        }
267
268                        @Override
269                        public void onChecked(boolean matched, int timeoutMs) {
270                            if (LatencyTracker.isEnabled(mContext)) {
271                                LatencyTracker.getInstance(mContext).onActionEnd(
272                                        ACTION_CHECK_CREDENTIAL_UNLOCKED);
273                            }
274                            mLockPatternView.enableInput();
275                            mPendingLockCheck = null;
276                            if (!matched) {
277                                onPatternChecked(userId, false /* matched */, timeoutMs,
278                                        true /* isValidPattern */);
279                            }
280                        }
281
282                        @Override
283                        public void onCancelled() {
284                            // We already got dismissed with the early matched callback, so we
285                            // cancelled the check. However, we still need to note down the latency.
286                            if (LatencyTracker.isEnabled(mContext)) {
287                                LatencyTracker.getInstance(mContext).onActionEnd(
288                                        ACTION_CHECK_CREDENTIAL_UNLOCKED);
289                            }
290                        }
291                    });
292            if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) {
293                mCallback.userActivity();
294            }
295        }
296
297        private void onPatternChecked(int userId, boolean matched, int timeoutMs,
298                boolean isValidPattern) {
299            boolean dismissKeyguard = KeyguardUpdateMonitor.getCurrentUser() == userId;
300            if (matched) {
301                mCallback.reportUnlockAttempt(userId, true, 0);
302                if (dismissKeyguard) {
303                    mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct);
304                    mCallback.dismiss(true, userId);
305                }
306            } else {
307                mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
308                if (isValidPattern) {
309                    mCallback.reportUnlockAttempt(userId, false, timeoutMs);
310                    if (timeoutMs > 0) {
311                        long deadline = mLockPatternUtils.setLockoutAttemptDeadline(
312                                userId, timeoutMs);
313                        handleAttemptLockout(deadline);
314                    }
315                }
316                if (timeoutMs == 0) {
317                    mSecurityMessageDisplay.setMessage(R.string.kg_wrong_pattern);
318                    mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS);
319                }
320            }
321        }
322    }
323
324    private void handleAttemptLockout(long elapsedRealtimeDeadline) {
325        mLockPatternView.clearPattern();
326        mLockPatternView.setEnabled(false);
327        final long elapsedRealtime = SystemClock.elapsedRealtime();
328        final long secondsInFuture = (long) Math.ceil(
329                (elapsedRealtimeDeadline - elapsedRealtime) / 1000.0);
330        mCountdownTimer = new CountDownTimer(secondsInFuture * 1000, 1000) {
331
332            @Override
333            public void onTick(long millisUntilFinished) {
334                final int secondsRemaining = (int) Math.round(millisUntilFinished / 1000.0);
335                mSecurityMessageDisplay.formatMessage(
336                        R.string.kg_too_many_failed_attempts_countdown, secondsRemaining);
337            }
338
339            @Override
340            public void onFinish() {
341                mLockPatternView.setEnabled(true);
342                displayDefaultSecurityMessage();
343            }
344
345        }.start();
346    }
347
348    @Override
349    public boolean needsInput() {
350        return false;
351    }
352
353    @Override
354    public void onPause() {
355        if (mCountdownTimer != null) {
356            mCountdownTimer.cancel();
357            mCountdownTimer = null;
358        }
359        if (mPendingLockCheck != null) {
360            mPendingLockCheck.cancel(false);
361            mPendingLockCheck = null;
362        }
363    }
364
365    @Override
366    public void onResume(int reason) {
367        reset();
368    }
369
370    @Override
371    public KeyguardSecurityCallback getCallback() {
372        return mCallback;
373    }
374
375    @Override
376    public void showPromptReason(int reason) {
377        switch (reason) {
378            case PROMPT_REASON_RESTART:
379                mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_restart_pattern);
380                break;
381            case PROMPT_REASON_TIMEOUT:
382                mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_timeout_pattern);
383                break;
384            case PROMPT_REASON_DEVICE_ADMIN:
385                mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_device_admin);
386                break;
387            case PROMPT_REASON_USER_REQUEST:
388                mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_user_request);
389                break;
390            case PROMPT_REASON_NONE:
391                break;
392            default:
393                mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_timeout_pattern);
394                break;
395        }
396    }
397
398    @Override
399    public void showMessage(String message, int color) {
400        mSecurityMessageDisplay.setNextMessageColor(color);
401        mSecurityMessageDisplay.setMessage(message);
402    }
403
404    @Override
405    public void startAppearAnimation() {
406        enableClipping(false);
407        setAlpha(1f);
408        setTranslationY(mAppearAnimationUtils.getStartTranslation());
409        AppearAnimationUtils.startTranslationYAnimation(this, 0 /* delay */, 500 /* duration */,
410                0, mAppearAnimationUtils.getInterpolator());
411        mAppearAnimationUtils.startAnimation2d(
412                mLockPatternView.getCellStates(),
413                new Runnable() {
414                    @Override
415                    public void run() {
416                        enableClipping(true);
417                    }
418                },
419                this);
420        if (!TextUtils.isEmpty(mSecurityMessageDisplay.getText())) {
421            mAppearAnimationUtils.createAnimation(mSecurityMessageDisplay, 0,
422                    AppearAnimationUtils.DEFAULT_APPEAR_DURATION,
423                    mAppearAnimationUtils.getStartTranslation(),
424                    true /* appearing */,
425                    mAppearAnimationUtils.getInterpolator(),
426                    null /* finishRunnable */);
427        }
428    }
429
430    @Override
431    public boolean startDisappearAnimation(final Runnable finishRunnable) {
432        float durationMultiplier = mKeyguardUpdateMonitor.needsSlowUnlockTransition()
433                ? DISAPPEAR_MULTIPLIER_LOCKED
434                : 1f;
435        mLockPatternView.clearPattern();
436        enableClipping(false);
437        setTranslationY(0);
438        AppearAnimationUtils.startTranslationYAnimation(this, 0 /* delay */,
439                (long) (300 * durationMultiplier),
440                -mDisappearAnimationUtils.getStartTranslation(),
441                mDisappearAnimationUtils.getInterpolator());
442
443        DisappearAnimationUtils disappearAnimationUtils = mKeyguardUpdateMonitor
444                .needsSlowUnlockTransition()
445                        ? mDisappearAnimationUtilsLocked
446                        : mDisappearAnimationUtils;
447        disappearAnimationUtils.startAnimation2d(mLockPatternView.getCellStates(),
448                () -> {
449                    enableClipping(true);
450                    if (finishRunnable != null) {
451                        finishRunnable.run();
452                    }
453                }, KeyguardPatternView.this);
454        if (!TextUtils.isEmpty(mSecurityMessageDisplay.getText())) {
455            mDisappearAnimationUtils.createAnimation(mSecurityMessageDisplay, 0,
456                    (long) (200 * durationMultiplier),
457                    - mDisappearAnimationUtils.getStartTranslation() * 3,
458                    false /* appearing */,
459                    mDisappearAnimationUtils.getInterpolator(),
460                    null /* finishRunnable */);
461        }
462        return true;
463    }
464
465    private void enableClipping(boolean enable) {
466        setClipChildren(enable);
467        mContainer.setClipToPadding(enable);
468        mContainer.setClipChildren(enable);
469    }
470
471    @Override
472    public void createAnimation(final LockPatternView.CellState animatedCell, long delay,
473            long duration, float translationY, final boolean appearing,
474            Interpolator interpolator,
475            final Runnable finishListener) {
476        mLockPatternView.startCellStateAnimation(animatedCell,
477                1f, appearing ? 1f : 0f, /* alpha */
478                appearing ? translationY : 0f, appearing ? 0f : translationY, /* translation */
479                appearing ? 0f : 1f, 1f /* scale */,
480                delay, duration, interpolator, finishListener);
481        if (finishListener != null) {
482            // Also animate the Emergency call
483            mAppearAnimationUtils.createAnimation(mEcaView, delay, duration, translationY,
484                    appearing, interpolator, null);
485        }
486    }
487
488    @Override
489    public boolean hasOverlappingRendering() {
490        return false;
491    }
492}
493