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