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