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