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