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