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  */
16 package com.android.keyguard;
17 
18 import android.animation.Animator;
19 import android.animation.AnimatorListenerAdapter;
20 import android.animation.ValueAnimator;
21 import android.content.Context;
22 import android.graphics.Rect;
23 import android.graphics.drawable.Drawable;
24 import android.os.CountDownTimer;
25 import android.os.SystemClock;
26 import android.text.TextUtils;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 import android.view.MotionEvent;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.view.animation.AnimationUtils;
33 import android.view.animation.Interpolator;
34 import android.widget.LinearLayout;
35 
36 import com.android.internal.widget.LockPatternUtils;
37 import com.android.internal.widget.LockPatternView;
38 
39 import java.util.List;
40 
41 public 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 
KeyguardPatternView(Context context)95     public KeyguardPatternView(Context context) {
96         this(context, null);
97     }
98 
KeyguardPatternView(Context context, AttributeSet attrs)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 
setKeyguardCallback(KeyguardSecurityCallback callback)114     public void setKeyguardCallback(KeyguardSecurityCallback callback) {
115         mCallback = callback;
116     }
117 
setLockPatternUtils(LockPatternUtils utils)118     public void setLockPatternUtils(LockPatternUtils utils) {
119         mLockPatternUtils = utils;
120     }
121 
122     @Override
onFinishInflate()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
onTouchEvent(MotionEvent ev)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 
reset()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 
displayDefaultSecurityMessage()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
showUsabilityHint()193     public void showUsabilityHint() {
194     }
195 
196     /** TODO: hook this up */
cleanUp()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 
onPatternStart()205         public void onPatternStart() {
206             mLockPatternView.removeCallbacks(mCancelPatternRunnable);
207         }
208 
onPatternCleared()209         public void onPatternCleared() {
210         }
211 
onPatternCellAdded(List<LockPatternView.Cell> pattern)212         public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
213             mCallback.userActivity();
214         }
215 
onPatternDetected(List<LockPatternView.Cell> pattern)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 
handleAttemptLockout(long elapsedRealtimeDeadline)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
needsInput()268     public boolean needsInput() {
269         return false;
270     }
271 
272     @Override
onPause()273     public void onPause() {
274         if (mCountdownTimer != null) {
275             mCountdownTimer.cancel();
276             mCountdownTimer = null;
277         }
278     }
279 
280     @Override
onResume(int reason)281     public void onResume(int reason) {
282         reset();
283     }
284 
285     @Override
getCallback()286     public KeyguardSecurityCallback getCallback() {
287         return mCallback;
288     }
289 
290     @Override
showBouncer(int duration)291     public void showBouncer(int duration) {
292         KeyguardSecurityViewHelper.
293                 showBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration);
294     }
295 
296     @Override
hideBouncer(int duration)297     public void hideBouncer(int duration) {
298         KeyguardSecurityViewHelper.
299                 hideBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration);
300     }
301 
302     @Override
startAppearAnimation()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
startDisappearAnimation(final Runnable finishRunnable)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 
enableClipping(boolean enable)360     private void enableClipping(boolean enable) {
361         setClipChildren(enable);
362         mKeyguardBouncerFrame.setClipToPadding(enable);
363         mKeyguardBouncerFrame.setClipChildren(enable);
364     }
365 
366     @Override
createAnimation(final LockPatternView.CellState animatedCell, long delay, long duration, float translationY, final boolean appearing, Interpolator interpolator, final Runnable finishListener)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
hasOverlappingRendering()411     public boolean hasOverlappingRendering() {
412         return false;
413     }
414 }
415