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.animation.Animator; 19 import android.animation.AnimatorListenerAdapter; 20 import android.animation.ValueAnimator; 21 import android.content.Context; 22 import android.graphics.Rect; 23 import android.graphics.drawable.Drawable; 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.widget.LockPatternUtils; 37 import com.android.internal.widget.LockPatternView; 38 39 import java.util.List; 40 41 public class KeyguardPatternView extends LinearLayout implements KeyguardSecurityView, 42 AppearAnimationCreator<LockPatternView.CellState> { 43 44 private static final String TAG = "SecurityPatternView"; 45 private static final boolean DEBUG = KeyguardConstants.DEBUG; 46 47 // how long before we clear the wrong pattern 48 private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000; 49 50 // how long we stay awake after each key beyond MIN_PATTERN_BEFORE_POKE_WAKELOCK 51 private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000; 52 53 // how many cells the user has to cross before we poke the wakelock 54 private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2; 55 56 private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; 57 private final AppearAnimationUtils mAppearAnimationUtils; 58 private final DisappearAnimationUtils mDisappearAnimationUtils; 59 60 private CountDownTimer mCountdownTimer = null; 61 private LockPatternUtils mLockPatternUtils; 62 private LockPatternView mLockPatternView; 63 private KeyguardSecurityCallback mCallback; 64 65 /** 66 * Keeps track of the last time we poked the wake lock during dispatching of the touch event. 67 * Initialized to something guaranteed to make us poke the wakelock when the user starts 68 * drawing the pattern. 69 * @see #dispatchTouchEvent(android.view.MotionEvent) 70 */ 71 private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS; 72 73 /** 74 * Useful for clearing out the wrong pattern after a delay 75 */ 76 private Runnable mCancelPatternRunnable = new Runnable() { 77 public void run() { 78 mLockPatternView.clearPattern(); 79 } 80 }; 81 private Rect mTempRect = new Rect(); 82 private SecurityMessageDisplay mSecurityMessageDisplay; 83 private View mEcaView; 84 private Drawable mBouncerFrame; 85 private ViewGroup mKeyguardBouncerFrame; 86 private KeyguardMessageArea mHelpMessage; 87 private int mDisappearYTranslation; 88 89 enum FooterMode { 90 Normal, 91 ForgotLockPattern, 92 VerifyUnlocked 93 } 94 KeyguardPatternView(Context context)95 public KeyguardPatternView(Context context) { 96 this(context, null); 97 } 98 KeyguardPatternView(Context context, AttributeSet attrs)99 public KeyguardPatternView(Context context, AttributeSet attrs) { 100 super(context, attrs); 101 mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext); 102 mAppearAnimationUtils = new AppearAnimationUtils(context, 103 AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 1.5f /* translationScale */, 104 2.0f /* delayScale */, AnimationUtils.loadInterpolator( 105 mContext, android.R.interpolator.linear_out_slow_in)); 106 mDisappearAnimationUtils = new DisappearAnimationUtils(context, 107 125, 1.2f /* translationScale */, 108 0.8f /* delayScale */, AnimationUtils.loadInterpolator( 109 mContext, android.R.interpolator.fast_out_linear_in)); 110 mDisappearYTranslation = getResources().getDimensionPixelSize( 111 R.dimen.disappear_y_translation); 112 } 113 setKeyguardCallback(KeyguardSecurityCallback callback)114 public void setKeyguardCallback(KeyguardSecurityCallback callback) { 115 mCallback = callback; 116 } 117 setLockPatternUtils(LockPatternUtils utils)118 public void setLockPatternUtils(LockPatternUtils utils) { 119 mLockPatternUtils = utils; 120 } 121 122 @Override onFinishInflate()123 protected void onFinishInflate() { 124 super.onFinishInflate(); 125 mLockPatternUtils = mLockPatternUtils == null 126 ? new LockPatternUtils(mContext) : mLockPatternUtils; 127 128 mLockPatternView = (LockPatternView) findViewById(R.id.lockPatternView); 129 mLockPatternView.setSaveEnabled(false); 130 mLockPatternView.setFocusable(false); 131 mLockPatternView.setOnPatternListener(new UnlockPatternListener()); 132 133 // stealth mode will be the same for the life of this screen 134 mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled()); 135 136 // vibrate mode will be the same for the life of this screen 137 mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled()); 138 139 setFocusableInTouchMode(true); 140 141 mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this); 142 mEcaView = findViewById(R.id.keyguard_selector_fade_container); 143 View bouncerFrameView = findViewById(R.id.keyguard_bouncer_frame); 144 if (bouncerFrameView != null) { 145 mBouncerFrame = bouncerFrameView.getBackground(); 146 } 147 148 mKeyguardBouncerFrame = (ViewGroup) findViewById(R.id.keyguard_bouncer_frame); 149 mHelpMessage = (KeyguardMessageArea) findViewById(R.id.keyguard_message_area); 150 } 151 152 @Override onTouchEvent(MotionEvent ev)153 public boolean onTouchEvent(MotionEvent ev) { 154 boolean result = super.onTouchEvent(ev); 155 // as long as the user is entering a pattern (i.e sending a touch event that was handled 156 // by this screen), keep poking the wake lock so that the screen will stay on. 157 final long elapsed = SystemClock.elapsedRealtime() - mLastPokeTime; 158 if (result && (elapsed > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) { 159 mLastPokeTime = SystemClock.elapsedRealtime(); 160 } 161 mTempRect.set(0, 0, 0, 0); 162 offsetRectIntoDescendantCoords(mLockPatternView, mTempRect); 163 ev.offsetLocation(mTempRect.left, mTempRect.top); 164 result = mLockPatternView.dispatchTouchEvent(ev) || result; 165 ev.offsetLocation(-mTempRect.left, -mTempRect.top); 166 return result; 167 } 168 reset()169 public void reset() { 170 // reset lock pattern 171 mLockPatternView.enableInput(); 172 mLockPatternView.setEnabled(true); 173 mLockPatternView.clearPattern(); 174 175 // if the user is currently locked out, enforce it. 176 long deadline = mLockPatternUtils.getLockoutAttemptDeadline(); 177 if (deadline != 0) { 178 handleAttemptLockout(deadline); 179 } else { 180 displayDefaultSecurityMessage(); 181 } 182 } 183 displayDefaultSecurityMessage()184 private void displayDefaultSecurityMessage() { 185 if (mKeyguardUpdateMonitor.getMaxBiometricUnlockAttemptsReached()) { 186 mSecurityMessageDisplay.setMessage(R.string.faceunlock_multiple_failures, true); 187 } else { 188 mSecurityMessageDisplay.setMessage(R.string.kg_pattern_instructions, false); 189 } 190 } 191 192 @Override showUsabilityHint()193 public void showUsabilityHint() { 194 } 195 196 /** TODO: hook this up */ cleanUp()197 public void cleanUp() { 198 if (DEBUG) Log.v(TAG, "Cleanup() called on " + this); 199 mLockPatternUtils = null; 200 mLockPatternView.setOnPatternListener(null); 201 } 202 203 private class UnlockPatternListener implements LockPatternView.OnPatternListener { 204 onPatternStart()205 public void onPatternStart() { 206 mLockPatternView.removeCallbacks(mCancelPatternRunnable); 207 } 208 onPatternCleared()209 public void onPatternCleared() { 210 } 211 onPatternCellAdded(List<LockPatternView.Cell> pattern)212 public void onPatternCellAdded(List<LockPatternView.Cell> pattern) { 213 mCallback.userActivity(); 214 } 215 onPatternDetected(List<LockPatternView.Cell> pattern)216 public void onPatternDetected(List<LockPatternView.Cell> pattern) { 217 if (mLockPatternUtils.checkPattern(pattern)) { 218 mCallback.reportUnlockAttempt(true); 219 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct); 220 mCallback.dismiss(true); 221 } else { 222 if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { 223 mCallback.userActivity(); 224 } 225 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); 226 boolean registeredAttempt = 227 pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL; 228 if (registeredAttempt) { 229 mCallback.reportUnlockAttempt(false); 230 } 231 int attempts = mKeyguardUpdateMonitor.getFailedUnlockAttempts(); 232 if (registeredAttempt && 233 0 == (attempts % LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) { 234 long deadline = mLockPatternUtils.setLockoutAttemptDeadline(); 235 handleAttemptLockout(deadline); 236 } else { 237 mSecurityMessageDisplay.setMessage(R.string.kg_wrong_pattern, true); 238 mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS); 239 } 240 } 241 } 242 } 243 handleAttemptLockout(long elapsedRealtimeDeadline)244 private void handleAttemptLockout(long elapsedRealtimeDeadline) { 245 mLockPatternView.clearPattern(); 246 mLockPatternView.setEnabled(false); 247 final long elapsedRealtime = SystemClock.elapsedRealtime(); 248 249 mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) { 250 251 @Override 252 public void onTick(long millisUntilFinished) { 253 final int secondsRemaining = (int) (millisUntilFinished / 1000); 254 mSecurityMessageDisplay.setMessage( 255 R.string.kg_too_many_failed_attempts_countdown, true, secondsRemaining); 256 } 257 258 @Override 259 public void onFinish() { 260 mLockPatternView.setEnabled(true); 261 displayDefaultSecurityMessage(); 262 } 263 264 }.start(); 265 } 266 267 @Override needsInput()268 public boolean needsInput() { 269 return false; 270 } 271 272 @Override onPause()273 public void onPause() { 274 if (mCountdownTimer != null) { 275 mCountdownTimer.cancel(); 276 mCountdownTimer = null; 277 } 278 } 279 280 @Override onResume(int reason)281 public void onResume(int reason) { 282 reset(); 283 } 284 285 @Override getCallback()286 public KeyguardSecurityCallback getCallback() { 287 return mCallback; 288 } 289 290 @Override showBouncer(int duration)291 public void showBouncer(int duration) { 292 KeyguardSecurityViewHelper. 293 showBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); 294 } 295 296 @Override hideBouncer(int duration)297 public void hideBouncer(int duration) { 298 KeyguardSecurityViewHelper. 299 hideBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); 300 } 301 302 @Override startAppearAnimation()303 public void startAppearAnimation() { 304 enableClipping(false); 305 setAlpha(1f); 306 setTranslationY(mAppearAnimationUtils.getStartTranslation()); 307 animate() 308 .setDuration(500) 309 .setInterpolator(mAppearAnimationUtils.getInterpolator()) 310 .translationY(0); 311 mAppearAnimationUtils.startAnimation( 312 mLockPatternView.getCellStates(), 313 new Runnable() { 314 @Override 315 public void run() { 316 enableClipping(true); 317 } 318 }, 319 this); 320 if (!TextUtils.isEmpty(mHelpMessage.getText())) { 321 mAppearAnimationUtils.createAnimation(mHelpMessage, 0, 322 AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 323 mAppearAnimationUtils.getStartTranslation(), 324 true /* appearing */, 325 mAppearAnimationUtils.getInterpolator(), 326 null /* finishRunnable */); 327 } 328 } 329 330 @Override startDisappearAnimation(final Runnable finishRunnable)331 public boolean startDisappearAnimation(final Runnable finishRunnable) { 332 mLockPatternView.clearPattern(); 333 enableClipping(false); 334 setTranslationY(0); 335 animate() 336 .setDuration(300) 337 .setInterpolator(mDisappearAnimationUtils.getInterpolator()) 338 .translationY(-mDisappearAnimationUtils.getStartTranslation()); 339 mDisappearAnimationUtils.startAnimation(mLockPatternView.getCellStates(), 340 new Runnable() { 341 @Override 342 public void run() { 343 enableClipping(true); 344 if (finishRunnable != null) { 345 finishRunnable.run(); 346 } 347 } 348 }, KeyguardPatternView.this); 349 if (!TextUtils.isEmpty(mHelpMessage.getText())) { 350 mDisappearAnimationUtils.createAnimation(mHelpMessage, 0, 351 200, 352 - mDisappearAnimationUtils.getStartTranslation() * 3, 353 false /* appearing */, 354 mDisappearAnimationUtils.getInterpolator(), 355 null /* finishRunnable */); 356 } 357 return true; 358 } 359 enableClipping(boolean enable)360 private void enableClipping(boolean enable) { 361 setClipChildren(enable); 362 mKeyguardBouncerFrame.setClipToPadding(enable); 363 mKeyguardBouncerFrame.setClipChildren(enable); 364 } 365 366 @Override createAnimation(final LockPatternView.CellState animatedCell, long delay, long duration, float translationY, final boolean appearing, Interpolator interpolator, final Runnable finishListener)367 public void createAnimation(final LockPatternView.CellState animatedCell, long delay, 368 long duration, float translationY, final boolean appearing, 369 Interpolator interpolator, 370 final Runnable finishListener) { 371 if (appearing) { 372 animatedCell.scale = 0.0f; 373 animatedCell.alpha = 1.0f; 374 } 375 animatedCell.translateY = appearing ? translationY : 0; 376 ValueAnimator animator = ValueAnimator.ofFloat(animatedCell.translateY, 377 appearing ? 0 : translationY); 378 animator.setInterpolator(interpolator); 379 animator.setDuration(duration); 380 animator.setStartDelay(delay); 381 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 382 @Override 383 public void onAnimationUpdate(ValueAnimator animation) { 384 float animatedFraction = animation.getAnimatedFraction(); 385 if (appearing) { 386 animatedCell.scale = animatedFraction; 387 } else { 388 animatedCell.alpha = 1 - animatedFraction; 389 } 390 animatedCell.translateY = (float) animation.getAnimatedValue(); 391 mLockPatternView.invalidate(); 392 } 393 }); 394 if (finishListener != null) { 395 animator.addListener(new AnimatorListenerAdapter() { 396 @Override 397 public void onAnimationEnd(Animator animation) { 398 finishListener.run(); 399 } 400 }); 401 402 // Also animate the Emergency call 403 mAppearAnimationUtils.createAnimation(mEcaView, delay, duration, translationY, 404 appearing, interpolator, null); 405 } 406 animator.start(); 407 mLockPatternView.invalidate(); 408 } 409 410 @Override hasOverlappingRendering()411 public boolean hasOverlappingRendering() { 412 return false; 413 } 414 } 415