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