KeyguardPatternView.java revision fb28c0e129896b17e4906b873bf44eb103ac2a8d
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.accounts.Account; 19import android.accounts.AccountManager; 20import android.accounts.AccountManagerCallback; 21import android.accounts.AccountManagerFuture; 22import android.accounts.AuthenticatorException; 23import android.accounts.OperationCanceledException; 24import android.animation.Animator; 25import android.animation.AnimatorListenerAdapter; 26import android.animation.ValueAnimator; 27import android.content.Context; 28import android.graphics.Rect; 29import android.graphics.drawable.Drawable; 30import android.os.Bundle; 31import android.os.CountDownTimer; 32import android.os.SystemClock; 33import android.os.UserHandle; 34import android.text.TextUtils; 35import android.util.AttributeSet; 36import android.util.Log; 37import android.view.MotionEvent; 38import android.view.View; 39import android.view.ViewGroup; 40import android.view.animation.AccelerateInterpolator; 41import android.view.animation.AnimationUtils; 42import android.view.animation.Interpolator; 43import android.widget.Button; 44import android.widget.LinearLayout; 45 46import com.android.internal.widget.LockPatternUtils; 47import com.android.internal.widget.LockPatternView; 48 49import java.io.IOException; 50import java.util.List; 51 52public class KeyguardPatternView extends LinearLayout implements KeyguardSecurityView, 53 AppearAnimationCreator<LockPatternView.CellState> { 54 55 private static final String TAG = "SecurityPatternView"; 56 private static final boolean DEBUG = KeyguardConstants.DEBUG; 57 58 // how long before we clear the wrong pattern 59 private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000; 60 61 // how long we stay awake after each key beyond MIN_PATTERN_BEFORE_POKE_WAKELOCK 62 private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000; 63 64 // how many cells the user has to cross before we poke the wakelock 65 private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2; 66 67 private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; 68 private final AppearAnimationUtils mAppearAnimationUtils; 69 70 private CountDownTimer mCountdownTimer = null; 71 private LockPatternUtils mLockPatternUtils; 72 private LockPatternView mLockPatternView; 73 private Button mForgotPatternButton; 74 private KeyguardSecurityCallback mCallback; 75 private boolean mEnableFallback; 76 77 /** 78 * Keeps track of the last time we poked the wake lock during dispatching of the touch event. 79 * Initialized to something guaranteed to make us poke the wakelock when the user starts 80 * drawing the pattern. 81 * @see #dispatchTouchEvent(android.view.MotionEvent) 82 */ 83 private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS; 84 85 /** 86 * Useful for clearing out the wrong pattern after a delay 87 */ 88 private Runnable mCancelPatternRunnable = new Runnable() { 89 public void run() { 90 mLockPatternView.clearPattern(); 91 } 92 }; 93 private Rect mTempRect = new Rect(); 94 private SecurityMessageDisplay mSecurityMessageDisplay; 95 private View mEcaView; 96 private Drawable mBouncerFrame; 97 private ViewGroup mKeyguardBouncerFrame; 98 private KeyguardMessageArea mHelpMessage; 99 private int mDisappearYTranslation; 100 101 enum FooterMode { 102 Normal, 103 ForgotLockPattern, 104 VerifyUnlocked 105 } 106 107 public KeyguardPatternView(Context context) { 108 this(context, null); 109 } 110 111 public KeyguardPatternView(Context context, AttributeSet attrs) { 112 super(context, attrs); 113 mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext); 114 mAppearAnimationUtils = new AppearAnimationUtils(context, 115 AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 1.5f /* delayScale */, 116 2.0f /* transitionScale */, AnimationUtils.loadInterpolator( 117 mContext, android.R.interpolator.linear_out_slow_in)); 118 mDisappearYTranslation = getResources().getDimensionPixelSize( 119 R.dimen.disappear_y_translation); 120 } 121 122 public void setKeyguardCallback(KeyguardSecurityCallback callback) { 123 mCallback = callback; 124 } 125 126 public void setLockPatternUtils(LockPatternUtils utils) { 127 mLockPatternUtils = utils; 128 } 129 130 @Override 131 protected void onFinishInflate() { 132 super.onFinishInflate(); 133 mLockPatternUtils = mLockPatternUtils == null 134 ? new LockPatternUtils(mContext) : mLockPatternUtils; 135 136 mLockPatternView = (LockPatternView) findViewById(R.id.lockPatternView); 137 mLockPatternView.setSaveEnabled(false); 138 mLockPatternView.setFocusable(false); 139 mLockPatternView.setOnPatternListener(new UnlockPatternListener()); 140 141 // stealth mode will be the same for the life of this screen 142 mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled()); 143 144 // vibrate mode will be the same for the life of this screen 145 mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled()); 146 147 mForgotPatternButton = (Button) findViewById(R.id.forgot_password_button); 148 // note: some configurations don't have an emergency call area 149 if (mForgotPatternButton != null) { 150 mForgotPatternButton.setText(R.string.kg_forgot_pattern_button_text); 151 mForgotPatternButton.setOnClickListener(new OnClickListener() { 152 public void onClick(View v) { 153 mCallback.showBackupSecurity(); 154 } 155 }); 156 } 157 158 setFocusableInTouchMode(true); 159 160 maybeEnableFallback(mContext); 161 mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this); 162 mEcaView = findViewById(R.id.keyguard_selector_fade_container); 163 View bouncerFrameView = findViewById(R.id.keyguard_bouncer_frame); 164 if (bouncerFrameView != null) { 165 mBouncerFrame = bouncerFrameView.getBackground(); 166 } 167 168 mKeyguardBouncerFrame = (ViewGroup) findViewById(R.id.keyguard_bouncer_frame); 169 mHelpMessage = (KeyguardMessageArea) findViewById(R.id.keyguard_message_area); 170 } 171 172 private void updateFooter(FooterMode mode) { 173 if (mForgotPatternButton == null) return; // no ECA? no footer 174 175 switch (mode) { 176 case Normal: 177 if (DEBUG) Log.d(TAG, "mode normal"); 178 mForgotPatternButton.setVisibility(View.GONE); 179 break; 180 case ForgotLockPattern: 181 if (DEBUG) Log.d(TAG, "mode ForgotLockPattern"); 182 mForgotPatternButton.setVisibility(View.VISIBLE); 183 break; 184 case VerifyUnlocked: 185 if (DEBUG) Log.d(TAG, "mode VerifyUnlocked"); 186 mForgotPatternButton.setVisibility(View.GONE); 187 } 188 } 189 190 @Override 191 public boolean onTouchEvent(MotionEvent ev) { 192 boolean result = super.onTouchEvent(ev); 193 // as long as the user is entering a pattern (i.e sending a touch event that was handled 194 // by this screen), keep poking the wake lock so that the screen will stay on. 195 final long elapsed = SystemClock.elapsedRealtime() - mLastPokeTime; 196 if (result && (elapsed > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) { 197 mLastPokeTime = SystemClock.elapsedRealtime(); 198 } 199 mTempRect.set(0, 0, 0, 0); 200 offsetRectIntoDescendantCoords(mLockPatternView, mTempRect); 201 ev.offsetLocation(mTempRect.left, mTempRect.top); 202 result = mLockPatternView.dispatchTouchEvent(ev) || result; 203 ev.offsetLocation(-mTempRect.left, -mTempRect.top); 204 return result; 205 } 206 207 public void reset() { 208 // reset lock pattern 209 mLockPatternView.enableInput(); 210 mLockPatternView.setEnabled(true); 211 mLockPatternView.clearPattern(); 212 213 // if the user is currently locked out, enforce it. 214 long deadline = mLockPatternUtils.getLockoutAttemptDeadline(); 215 if (deadline != 0) { 216 handleAttemptLockout(deadline); 217 } else { 218 displayDefaultSecurityMessage(); 219 } 220 221 // the footer depends on how many total attempts the user has failed 222 if (mCallback.isVerifyUnlockOnly()) { 223 updateFooter(FooterMode.VerifyUnlocked); 224 } else if (mEnableFallback && 225 (mKeyguardUpdateMonitor.getFailedUnlockAttempts() 226 >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) { 227 updateFooter(FooterMode.ForgotLockPattern); 228 } else { 229 updateFooter(FooterMode.Normal); 230 } 231 232 } 233 234 private void displayDefaultSecurityMessage() { 235 if (mKeyguardUpdateMonitor.getMaxBiometricUnlockAttemptsReached()) { 236 mSecurityMessageDisplay.setMessage(R.string.faceunlock_multiple_failures, true); 237 } else { 238 mSecurityMessageDisplay.setMessage(R.string.kg_pattern_instructions, false); 239 } 240 } 241 242 @Override 243 public void showUsabilityHint() { 244 } 245 246 /** TODO: hook this up */ 247 public void cleanUp() { 248 if (DEBUG) Log.v(TAG, "Cleanup() called on " + this); 249 mLockPatternUtils = null; 250 mLockPatternView.setOnPatternListener(null); 251 } 252 253 private class UnlockPatternListener implements LockPatternView.OnPatternListener { 254 255 public void onPatternStart() { 256 mLockPatternView.removeCallbacks(mCancelPatternRunnable); 257 } 258 259 public void onPatternCleared() { 260 } 261 262 public void onPatternCellAdded(List<LockPatternView.Cell> pattern) { 263 mCallback.userActivity(); 264 } 265 266 public void onPatternDetected(List<LockPatternView.Cell> pattern) { 267 if (mLockPatternUtils.checkPattern(pattern)) { 268 mCallback.reportUnlockAttempt(true); 269 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct); 270 mCallback.dismiss(true); 271 } else { 272 if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { 273 mCallback.userActivity(); 274 } 275 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); 276 boolean registeredAttempt = 277 pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL; 278 if (registeredAttempt) { 279 mCallback.reportUnlockAttempt(false); 280 } 281 int attempts = mKeyguardUpdateMonitor.getFailedUnlockAttempts(); 282 if (registeredAttempt && 283 0 == (attempts % LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) { 284 long deadline = mLockPatternUtils.setLockoutAttemptDeadline(); 285 handleAttemptLockout(deadline); 286 } else { 287 mSecurityMessageDisplay.setMessage(R.string.kg_wrong_pattern, true); 288 mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS); 289 } 290 } 291 } 292 } 293 294 private void maybeEnableFallback(Context context) { 295 // Ask the account manager if we have an account that can be used as a 296 // fallback in case the user forgets his pattern. 297 AccountAnalyzer accountAnalyzer = new AccountAnalyzer(AccountManager.get(context)); 298 accountAnalyzer.start(); 299 } 300 301 private class AccountAnalyzer implements AccountManagerCallback<Bundle> { 302 private final AccountManager mAccountManager; 303 private final Account[] mAccounts; 304 private int mAccountIndex; 305 306 private AccountAnalyzer(AccountManager accountManager) { 307 mAccountManager = accountManager; 308 mAccounts = accountManager.getAccountsByTypeAsUser("com.google", 309 new UserHandle(mLockPatternUtils.getCurrentUser())); 310 } 311 312 private void next() { 313 // if we are ready to enable the fallback or if we depleted the list of accounts 314 // then finish and get out 315 if (mEnableFallback || mAccountIndex >= mAccounts.length) { 316 return; 317 } 318 319 // lookup the confirmCredentials intent for the current account 320 mAccountManager.confirmCredentialsAsUser(mAccounts[mAccountIndex], null, null, this, 321 null, new UserHandle(mLockPatternUtils.getCurrentUser())); 322 } 323 324 public void start() { 325 mEnableFallback = false; 326 mAccountIndex = 0; 327 next(); 328 } 329 330 public void run(AccountManagerFuture<Bundle> future) { 331 try { 332 Bundle result = future.getResult(); 333 if (result.getParcelable(AccountManager.KEY_INTENT) != null) { 334 mEnableFallback = true; 335 } 336 } catch (OperationCanceledException e) { 337 // just skip the account if we are unable to query it 338 } catch (IOException e) { 339 // just skip the account if we are unable to query it 340 } catch (AuthenticatorException e) { 341 // just skip the account if we are unable to query it 342 } finally { 343 mAccountIndex++; 344 next(); 345 } 346 } 347 } 348 349 private void handleAttemptLockout(long elapsedRealtimeDeadline) { 350 mLockPatternView.clearPattern(); 351 mLockPatternView.setEnabled(false); 352 final long elapsedRealtime = SystemClock.elapsedRealtime(); 353 if (mEnableFallback) { 354 updateFooter(FooterMode.ForgotLockPattern); 355 } 356 357 mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) { 358 359 @Override 360 public void onTick(long millisUntilFinished) { 361 final int secondsRemaining = (int) (millisUntilFinished / 1000); 362 mSecurityMessageDisplay.setMessage( 363 R.string.kg_too_many_failed_attempts_countdown, true, secondsRemaining); 364 } 365 366 @Override 367 public void onFinish() { 368 mLockPatternView.setEnabled(true); 369 displayDefaultSecurityMessage(); 370 // TODO mUnlockIcon.setVisibility(View.VISIBLE); 371 if (mEnableFallback) { 372 updateFooter(FooterMode.ForgotLockPattern); 373 } else { 374 updateFooter(FooterMode.Normal); 375 } 376 } 377 378 }.start(); 379 } 380 381 @Override 382 public boolean needsInput() { 383 return false; 384 } 385 386 @Override 387 public void onPause() { 388 if (mCountdownTimer != null) { 389 mCountdownTimer.cancel(); 390 mCountdownTimer = null; 391 } 392 } 393 394 @Override 395 public void onResume(int reason) { 396 reset(); 397 } 398 399 @Override 400 public KeyguardSecurityCallback getCallback() { 401 return mCallback; 402 } 403 404 @Override 405 public void showBouncer(int duration) { 406 KeyguardSecurityViewHelper. 407 showBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); 408 } 409 410 @Override 411 public void hideBouncer(int duration) { 412 KeyguardSecurityViewHelper. 413 hideBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); 414 } 415 416 @Override 417 public void startAppearAnimation() { 418 enableClipping(false); 419 setAlpha(1f); 420 setTranslationY(mAppearAnimationUtils.getStartTranslation()); 421 animate() 422 .setDuration(500) 423 .setInterpolator(mAppearAnimationUtils.getInterpolator()) 424 .translationY(0); 425 mAppearAnimationUtils.startAppearAnimation( 426 mLockPatternView.getCellStates(), 427 new Runnable() { 428 @Override 429 public void run() { 430 enableClipping(true); 431 } 432 }, 433 this); 434 if (!TextUtils.isEmpty(mHelpMessage.getText())) { 435 mAppearAnimationUtils.createAnimation(mHelpMessage, 0, 436 AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 437 mAppearAnimationUtils.getStartTranslation(), 438 mAppearAnimationUtils.getInterpolator(), 439 null /* finishRunnable */); 440 } 441 } 442 443 @Override 444 public boolean startDisappearAnimation(Runnable finishRunnable) { 445 mLockPatternView.clearPattern(); 446 animate() 447 .alpha(0f) 448 .translationY(mDisappearYTranslation) 449 .setInterpolator(AnimationUtils.loadInterpolator( 450 mContext, android.R.interpolator.fast_out_linear_in)) 451 .setDuration(100) 452 .withEndAction(finishRunnable); 453 return true; 454 } 455 456 private void enableClipping(boolean enable) { 457 setClipChildren(enable); 458 mKeyguardBouncerFrame.setClipToPadding(enable); 459 mKeyguardBouncerFrame.setClipChildren(enable); 460 } 461 462 @Override 463 public void createAnimation(final LockPatternView.CellState animatedCell, long delay, 464 long duration, float startTranslationY, Interpolator interpolator, 465 final Runnable finishListener) { 466 animatedCell.scale = 0.0f; 467 animatedCell.translateY = startTranslationY; 468 ValueAnimator animator = ValueAnimator.ofFloat(startTranslationY, 0.0f); 469 animator.setInterpolator(interpolator); 470 animator.setDuration(duration); 471 animator.setStartDelay(delay); 472 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 473 @Override 474 public void onAnimationUpdate(ValueAnimator animation) { 475 float animatedFraction = animation.getAnimatedFraction(); 476 animatedCell.scale = animatedFraction; 477 animatedCell.translateY = (float) animation.getAnimatedValue(); 478 mLockPatternView.invalidate(); 479 } 480 }); 481 if (finishListener != null) { 482 animator.addListener(new AnimatorListenerAdapter() { 483 @Override 484 public void onAnimationEnd(Animator animation) { 485 finishListener.run(); 486 } 487 }); 488 489 // Also animate the Emergency call 490 mAppearAnimationUtils.createAnimation(mEcaView, delay, duration, startTranslationY, 491 interpolator, null); 492 493 // And the forgot pattern button 494 if (mForgotPatternButton != null 495 && mForgotPatternButton.getVisibility() == View.VISIBLE) { 496 mAppearAnimationUtils.createAnimation(mForgotPatternButton, delay, duration, 497 startTranslationY, interpolator, null); 498 } 499 } 500 animator.start(); 501 mLockPatternView.invalidate(); 502 } 503 504 @Override 505 public boolean hasOverlappingRendering() { 506 return false; 507 } 508} 509