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