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