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 */ 16package com.android.keyguard; 17 18import android.animation.Animator; 19import android.animation.AnimatorListenerAdapter; 20import android.animation.ValueAnimator; 21import android.content.Context; 22import android.graphics.Rect; 23import android.graphics.drawable.Drawable; 24import android.os.CountDownTimer; 25import android.os.SystemClock; 26import android.text.TextUtils; 27import android.util.AttributeSet; 28import android.util.Log; 29import android.view.MotionEvent; 30import android.view.View; 31import android.view.ViewGroup; 32import android.view.animation.AnimationUtils; 33import android.view.animation.Interpolator; 34import android.widget.LinearLayout; 35 36import com.android.internal.widget.LockPatternUtils; 37import com.android.internal.widget.LockPatternView; 38 39import java.util.List; 40 41public 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 95 public KeyguardPatternView(Context context) { 96 this(context, null); 97 } 98 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 114 public void setKeyguardCallback(KeyguardSecurityCallback callback) { 115 mCallback = callback; 116 } 117 118 public void setLockPatternUtils(LockPatternUtils utils) { 119 mLockPatternUtils = utils; 120 } 121 122 @Override 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 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 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 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 193 public void showUsabilityHint() { 194 } 195 196 /** TODO: hook this up */ 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 205 public void onPatternStart() { 206 mLockPatternView.removeCallbacks(mCancelPatternRunnable); 207 } 208 209 public void onPatternCleared() { 210 } 211 212 public void onPatternCellAdded(List<LockPatternView.Cell> pattern) { 213 mCallback.userActivity(); 214 } 215 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 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 268 public boolean needsInput() { 269 return false; 270 } 271 272 @Override 273 public void onPause() { 274 if (mCountdownTimer != null) { 275 mCountdownTimer.cancel(); 276 mCountdownTimer = null; 277 } 278 } 279 280 @Override 281 public void onResume(int reason) { 282 reset(); 283 } 284 285 @Override 286 public KeyguardSecurityCallback getCallback() { 287 return mCallback; 288 } 289 290 @Override 291 public void showBouncer(int duration) { 292 KeyguardSecurityViewHelper. 293 showBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); 294 } 295 296 @Override 297 public void hideBouncer(int duration) { 298 KeyguardSecurityViewHelper. 299 hideBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); 300 } 301 302 @Override 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 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 360 private void enableClipping(boolean enable) { 361 setClipChildren(enable); 362 mKeyguardBouncerFrame.setClipToPadding(enable); 363 mKeyguardBouncerFrame.setClipChildren(enable); 364 } 365 366 @Override 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 411 public boolean hasOverlappingRendering() { 412 return false; 413 } 414} 415