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