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