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 113 public KeyguardPatternView(Context context) { 114 this(context, null); 115 } 116 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 137 public void setKeyguardCallback(KeyguardSecurityCallback callback) { 138 mCallback = callback; 139 } 140 141 @Override 142 public void setLockPatternUtils(LockPatternUtils utils) { 143 mLockPatternUtils = utils; 144 } 145 146 @Override 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 179 protected void onAttachedToWindow() { 180 super.onAttachedToWindow(); 181 mSecurityMessageDisplay = KeyguardMessageArea.findSecurityMessageDisplay(this); 182 } 183 184 @Override 185 public void onEmergencyButtonClickedWhenInCall() { 186 mCallback.reset(); 187 } 188 189 @Override 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 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 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 239 private void displayDefaultSecurityMessage() { 240 if (mSecurityMessageDisplay != null) { 241 mSecurityMessageDisplay.setMessage(""); 242 } 243 } 244 245 @Override 246 public void showUsabilityHint() { 247 } 248 249 @Override 250 public boolean disallowInterceptTouch(MotionEvent event) { 251 return mLockPatternScreenBounds.contains((int) event.getRawX(), (int) event.getRawY()); 252 } 253 254 /** TODO: hook this up */ 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 264 public void onPatternStart() { 265 mLockPatternView.removeCallbacks(mCancelPatternRunnable); 266 mSecurityMessageDisplay.setMessage(""); 267 } 268 269 @Override 270 public void onPatternCleared() { 271 } 272 273 @Override 274 public void onPatternCellAdded(List<LockPatternView.Cell> pattern) { 275 mCallback.userActivity(); 276 } 277 278 @Override 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 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 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 394 public boolean needsInput() { 395 return false; 396 } 397 398 @Override 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 412 public void onResume(int reason) { 413 } 414 415 @Override 416 public KeyguardSecurityCallback getCallback() { 417 return mCallback; 418 } 419 420 @Override 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 444 public void showMessage(CharSequence message, ColorStateList colorState) { 445 mSecurityMessageDisplay.setNextMessageColor(colorState); 446 mSecurityMessageDisplay.setMessage(message); 447 } 448 449 @Override 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 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 510 private void enableClipping(boolean enable) { 511 setClipChildren(enable); 512 mContainer.setClipToPadding(enable); 513 mContainer.setClipChildren(enable); 514 } 515 516 @Override 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 534 public boolean hasOverlappingRendering() { 535 return false; 536 } 537 538 @Override 539 public CharSequence getTitle() { 540 return getContext().getString( 541 com.android.internal.R.string.keyguard_accessibility_pattern_unlock); 542 } 543 } 544