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