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