KeyguardPatternView.java revision c0ae9e67ebe6f1298800feaed1b43e867139a904
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.internal.policy.impl.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.content.Context; 25import android.graphics.Rect; 26import android.graphics.drawable.Drawable; 27import android.os.Bundle; 28import android.os.CountDownTimer; 29import android.os.SystemClock; 30import android.util.AttributeSet; 31import android.util.Log; 32import android.view.MotionEvent; 33import android.view.View; 34import android.widget.Button; 35import android.widget.LinearLayout; 36 37import com.android.internal.widget.LockPatternUtils; 38import com.android.internal.widget.LockPatternView; 39import com.android.internal.R; 40 41import java.io.IOException; 42import java.util.List; 43 44public class KeyguardPatternView extends LinearLayout implements KeyguardSecurityView { 45 46 private static final String TAG = "SecurityPatternView"; 47 private static final boolean DEBUG = false; 48 49 // how long before we clear the wrong pattern 50 private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000; 51 52 // how long we stay awake after each key beyond MIN_PATTERN_BEFORE_POKE_WAKELOCK 53 private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000; 54 55 // how long we stay awake after the user hits the first dot. 56 private static final int UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS = 2000; 57 58 // how many cells the user has to cross before we poke the wakelock 59 private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2; 60 61 private int mFailedPatternAttemptsSinceLastTimeout = 0; 62 private int mTotalFailedPatternAttempts = 0; 63 private CountDownTimer mCountdownTimer = null; 64 private LockPatternUtils mLockPatternUtils; 65 private LockPatternView mLockPatternView; 66 private Button mForgotPatternButton; 67 private KeyguardSecurityCallback mCallback; 68 private boolean mEnableFallback; 69 70 /** 71 * Keeps track of the last time we poked the wake lock during dispatching of the touch event. 72 * Initialized to something guaranteed to make us poke the wakelock when the user starts 73 * drawing the pattern. 74 * @see #dispatchTouchEvent(android.view.MotionEvent) 75 */ 76 private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS; 77 78 /** 79 * Useful for clearing out the wrong pattern after a delay 80 */ 81 private Runnable mCancelPatternRunnable = new Runnable() { 82 public void run() { 83 mLockPatternView.clearPattern(); 84 } 85 }; 86 private Rect mTempRect = new Rect(); 87 private SecurityMessageDisplay mSecurityMessageDisplay; 88 private View mEcaView; 89 private Drawable mBouncerFrame; 90 91 enum FooterMode { 92 Normal, 93 ForgotLockPattern, 94 VerifyUnlocked 95 } 96 97 public KeyguardPatternView(Context context) { 98 this(context, null); 99 } 100 101 public KeyguardPatternView(Context context, AttributeSet attrs) { 102 super(context, attrs); 103 } 104 105 public void setKeyguardCallback(KeyguardSecurityCallback callback) { 106 mCallback = callback; 107 } 108 109 public void setLockPatternUtils(LockPatternUtils utils) { 110 mLockPatternUtils = utils; 111 } 112 113 @Override 114 protected void onFinishInflate() { 115 super.onFinishInflate(); 116 mLockPatternUtils = mLockPatternUtils == null 117 ? new LockPatternUtils(mContext) : mLockPatternUtils; 118 119 mLockPatternView = (LockPatternView) findViewById(R.id.lockPatternView); 120 mLockPatternView.setSaveEnabled(false); 121 mLockPatternView.setFocusable(false); 122 mLockPatternView.setOnPatternListener(new UnlockPatternListener()); 123 124 // stealth mode will be the same for the life of this screen 125 mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled()); 126 127 // vibrate mode will be the same for the life of this screen 128 mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled()); 129 130 mForgotPatternButton = (Button) findViewById(R.id.forgot_password_button); 131 mForgotPatternButton.setText(R.string.kg_forgot_pattern_button_text); 132 mForgotPatternButton.setOnClickListener(new OnClickListener() { 133 public void onClick(View v) { 134 mCallback.showBackupSecurity(); 135 } 136 }); 137 138 setFocusableInTouchMode(true); 139 140 maybeEnableFallback(mContext); 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 149 private void updateFooter(FooterMode mode) { 150 switch (mode) { 151 case Normal: 152 if (DEBUG) Log.d(TAG, "mode normal"); 153 mForgotPatternButton.setVisibility(View.GONE); 154 break; 155 case ForgotLockPattern: 156 if (DEBUG) Log.d(TAG, "mode ForgotLockPattern"); 157 mForgotPatternButton.setVisibility(View.VISIBLE); 158 break; 159 case VerifyUnlocked: 160 if (DEBUG) Log.d(TAG, "mode VerifyUnlocked"); 161 mForgotPatternButton.setVisibility(View.GONE); 162 } 163 } 164 165 @Override 166 public boolean onTouchEvent(MotionEvent ev) { 167 boolean result = super.onTouchEvent(ev); 168 // as long as the user is entering a pattern (i.e sending a touch event that was handled 169 // by this screen), keep poking the wake lock so that the screen will stay on. 170 final long elapsed = SystemClock.elapsedRealtime() - mLastPokeTime; 171 if (result && (elapsed > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) { 172 mLastPokeTime = SystemClock.elapsedRealtime(); 173 } 174 mTempRect.set(0, 0, 0, 0); 175 offsetRectIntoDescendantCoords(mLockPatternView, mTempRect); 176 ev.offsetLocation(mTempRect.left, mTempRect.top); 177 result = mLockPatternView.dispatchTouchEvent(ev) || result; 178 ev.offsetLocation(-mTempRect.left, -mTempRect.top); 179 return result; 180 } 181 182 public void reset() { 183 // reset lock pattern 184 mLockPatternView.enableInput(); 185 mLockPatternView.setEnabled(true); 186 mLockPatternView.clearPattern(); 187 188 // if the user is currently locked out, enforce it. 189 long deadline = mLockPatternUtils.getLockoutAttemptDeadline(); 190 if (deadline != 0) { 191 handleAttemptLockout(deadline); 192 } else { 193 displayDefaultSecurityMessage(); 194 } 195 196 // the footer depends on how many total attempts the user has failed 197 if (mCallback.isVerifyUnlockOnly()) { 198 updateFooter(FooterMode.VerifyUnlocked); 199 } else if (mEnableFallback && 200 (mTotalFailedPatternAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) { 201 updateFooter(FooterMode.ForgotLockPattern); 202 } else { 203 updateFooter(FooterMode.Normal); 204 } 205 206 } 207 208 private void displayDefaultSecurityMessage() { 209 if (KeyguardUpdateMonitor.getInstance(mContext).getMaxBiometricUnlockAttemptsReached()) { 210 mSecurityMessageDisplay.setMessage(R.string.faceunlock_multiple_failures, true); 211 } else { 212 mSecurityMessageDisplay.setMessage(R.string.kg_pattern_instructions, false); 213 } 214 } 215 216 @Override 217 public void showUsabilityHint() { 218 } 219 220 /** TODO: hook this up */ 221 public void cleanUp() { 222 if (DEBUG) Log.v(TAG, "Cleanup() called on " + this); 223 mLockPatternUtils = null; 224 mLockPatternView.setOnPatternListener(null); 225 } 226 227 @Override 228 public void onWindowFocusChanged(boolean hasWindowFocus) { 229 super.onWindowFocusChanged(hasWindowFocus); 230 if (hasWindowFocus) { 231 // when timeout dialog closes we want to update our state 232 reset(); 233 } 234 } 235 236 private class UnlockPatternListener implements LockPatternView.OnPatternListener { 237 238 public void onPatternStart() { 239 mLockPatternView.removeCallbacks(mCancelPatternRunnable); 240 } 241 242 public void onPatternCleared() { 243 } 244 245 public void onPatternCellAdded(List<LockPatternView.Cell> pattern) { 246 // To guard against accidental poking of the wakelock, look for 247 // the user actually trying to draw a pattern of some minimal length. 248 if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { 249 mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_MS); 250 } else { 251 // Give just a little extra time if they hit one of the first few dots 252 mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS); 253 } 254 } 255 256 public void onPatternDetected(List<LockPatternView.Cell> pattern) { 257 if (mLockPatternUtils.checkPattern(pattern)) { 258 mCallback.reportSuccessfulUnlockAttempt(); 259 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct); 260 mTotalFailedPatternAttempts = 0; 261 mCallback.dismiss(true); 262 } else { 263 if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { 264 mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_MS); 265 } 266 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); 267 if (pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) { 268 mTotalFailedPatternAttempts++; 269 mFailedPatternAttemptsSinceLastTimeout++; 270 mCallback.reportFailedUnlockAttempt(); 271 } 272 if (mFailedPatternAttemptsSinceLastTimeout 273 >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) { 274 long deadline = mLockPatternUtils.setLockoutAttemptDeadline(); 275 handleAttemptLockout(deadline); 276 } else { 277 mSecurityMessageDisplay.setMessage(R.string.kg_wrong_pattern, true); 278 mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS); 279 } 280 } 281 } 282 } 283 284 private void maybeEnableFallback(Context context) { 285 // Ask the account manager if we have an account that can be used as a 286 // fallback in case the user forgets his pattern. 287 AccountAnalyzer accountAnalyzer = new AccountAnalyzer(AccountManager.get(context)); 288 accountAnalyzer.start(); 289 } 290 291 private class AccountAnalyzer implements AccountManagerCallback<Bundle> { 292 private final AccountManager mAccountManager; 293 private final Account[] mAccounts; 294 private int mAccountIndex; 295 296 private AccountAnalyzer(AccountManager accountManager) { 297 mAccountManager = accountManager; 298 mAccounts = accountManager.getAccountsByType("com.google"); 299 } 300 301 private void next() { 302 // if we are ready to enable the fallback or if we depleted the list of accounts 303 // then finish and get out 304 if (mEnableFallback || mAccountIndex >= mAccounts.length) { 305 return; 306 } 307 308 // lookup the confirmCredentials intent for the current account 309 mAccountManager.confirmCredentials(mAccounts[mAccountIndex], null, null, this, null); 310 } 311 312 public void start() { 313 mEnableFallback = false; 314 mAccountIndex = 0; 315 next(); 316 } 317 318 public void run(AccountManagerFuture<Bundle> future) { 319 try { 320 Bundle result = future.getResult(); 321 if (result.getParcelable(AccountManager.KEY_INTENT) != null) { 322 mEnableFallback = true; 323 } 324 } catch (OperationCanceledException e) { 325 // just skip the account if we are unable to query it 326 } catch (IOException e) { 327 // just skip the account if we are unable to query it 328 } catch (AuthenticatorException e) { 329 // just skip the account if we are unable to query it 330 } finally { 331 mAccountIndex++; 332 next(); 333 } 334 } 335 } 336 337 private void handleAttemptLockout(long elapsedRealtimeDeadline) { 338 mLockPatternView.clearPattern(); 339 mLockPatternView.setEnabled(false); 340 final long elapsedRealtime = SystemClock.elapsedRealtime(); 341 if (mEnableFallback) { 342 updateFooter(FooterMode.ForgotLockPattern); 343 } 344 345 mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) { 346 347 @Override 348 public void onTick(long millisUntilFinished) { 349 final int secondsRemaining = (int) (millisUntilFinished / 1000); 350 mSecurityMessageDisplay.setMessage( 351 R.string.kg_too_many_failed_attempts_countdown, true, secondsRemaining); 352 } 353 354 @Override 355 public void onFinish() { 356 mLockPatternView.setEnabled(true); 357 displayDefaultSecurityMessage(); 358 // TODO mUnlockIcon.setVisibility(View.VISIBLE); 359 mFailedPatternAttemptsSinceLastTimeout = 0; 360 if (mEnableFallback) { 361 updateFooter(FooterMode.ForgotLockPattern); 362 } else { 363 updateFooter(FooterMode.Normal); 364 } 365 } 366 367 }.start(); 368 } 369 370 @Override 371 public boolean needsInput() { 372 return false; 373 } 374 375 @Override 376 public void onPause() { 377 if (mCountdownTimer != null) { 378 mCountdownTimer.cancel(); 379 mCountdownTimer = null; 380 } 381 } 382 383 @Override 384 public void onResume() { 385 reset(); 386 } 387 388 @Override 389 public KeyguardSecurityCallback getCallback() { 390 return mCallback; 391 } 392 393 @Override 394 public void showBouncer(int duration) { 395 KeyguardSecurityViewHelper. 396 showBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); 397 } 398 399 @Override 400 public void hideBouncer(int duration) { 401 KeyguardSecurityViewHelper. 402 hideBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); 403 } 404} 405