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.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.os.UserHandle; 31import android.util.AttributeSet; 32import android.util.Log; 33import android.view.MotionEvent; 34import android.view.View; 35import android.widget.Button; 36import android.widget.LinearLayout; 37 38import com.android.internal.widget.LockPatternUtils; 39import com.android.internal.widget.LockPatternView; 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 // note: some configurations don't have an emergency call area 132 if (mForgotPatternButton != null) { 133 mForgotPatternButton.setText(R.string.kg_forgot_pattern_button_text); 134 mForgotPatternButton.setOnClickListener(new OnClickListener() { 135 public void onClick(View v) { 136 mCallback.showBackupSecurity(); 137 } 138 }); 139 } 140 141 setFocusableInTouchMode(true); 142 143 maybeEnableFallback(mContext); 144 mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this); 145 mEcaView = findViewById(R.id.keyguard_selector_fade_container); 146 View bouncerFrameView = findViewById(R.id.keyguard_bouncer_frame); 147 if (bouncerFrameView != null) { 148 mBouncerFrame = bouncerFrameView.getBackground(); 149 } 150 } 151 152 private void updateFooter(FooterMode mode) { 153 if (mForgotPatternButton == null) return; // no ECA? no footer 154 155 switch (mode) { 156 case Normal: 157 if (DEBUG) Log.d(TAG, "mode normal"); 158 mForgotPatternButton.setVisibility(View.GONE); 159 break; 160 case ForgotLockPattern: 161 if (DEBUG) Log.d(TAG, "mode ForgotLockPattern"); 162 mForgotPatternButton.setVisibility(View.VISIBLE); 163 break; 164 case VerifyUnlocked: 165 if (DEBUG) Log.d(TAG, "mode VerifyUnlocked"); 166 mForgotPatternButton.setVisibility(View.GONE); 167 } 168 } 169 170 @Override 171 public boolean onTouchEvent(MotionEvent ev) { 172 boolean result = super.onTouchEvent(ev); 173 // as long as the user is entering a pattern (i.e sending a touch event that was handled 174 // by this screen), keep poking the wake lock so that the screen will stay on. 175 final long elapsed = SystemClock.elapsedRealtime() - mLastPokeTime; 176 if (result && (elapsed > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) { 177 mLastPokeTime = SystemClock.elapsedRealtime(); 178 } 179 mTempRect.set(0, 0, 0, 0); 180 offsetRectIntoDescendantCoords(mLockPatternView, mTempRect); 181 ev.offsetLocation(mTempRect.left, mTempRect.top); 182 result = mLockPatternView.dispatchTouchEvent(ev) || result; 183 ev.offsetLocation(-mTempRect.left, -mTempRect.top); 184 return result; 185 } 186 187 public void reset() { 188 // reset lock pattern 189 mLockPatternView.enableInput(); 190 mLockPatternView.setEnabled(true); 191 mLockPatternView.clearPattern(); 192 193 // if the user is currently locked out, enforce it. 194 long deadline = mLockPatternUtils.getLockoutAttemptDeadline(); 195 if (deadline != 0) { 196 handleAttemptLockout(deadline); 197 } else { 198 displayDefaultSecurityMessage(); 199 } 200 201 // the footer depends on how many total attempts the user has failed 202 if (mCallback.isVerifyUnlockOnly()) { 203 updateFooter(FooterMode.VerifyUnlocked); 204 } else if (mEnableFallback && 205 (mTotalFailedPatternAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) { 206 updateFooter(FooterMode.ForgotLockPattern); 207 } else { 208 updateFooter(FooterMode.Normal); 209 } 210 211 } 212 213 private void displayDefaultSecurityMessage() { 214 if (KeyguardUpdateMonitor.getInstance(mContext).getMaxBiometricUnlockAttemptsReached()) { 215 mSecurityMessageDisplay.setMessage(R.string.faceunlock_multiple_failures, true); 216 } else { 217 mSecurityMessageDisplay.setMessage(R.string.kg_pattern_instructions, false); 218 } 219 } 220 221 @Override 222 public void showUsabilityHint() { 223 } 224 225 /** TODO: hook this up */ 226 public void cleanUp() { 227 if (DEBUG) Log.v(TAG, "Cleanup() called on " + this); 228 mLockPatternUtils = null; 229 mLockPatternView.setOnPatternListener(null); 230 } 231 232 @Override 233 public void onWindowFocusChanged(boolean hasWindowFocus) { 234 super.onWindowFocusChanged(hasWindowFocus); 235 if (hasWindowFocus) { 236 // when timeout dialog closes we want to update our state 237 reset(); 238 } 239 } 240 241 private class UnlockPatternListener implements LockPatternView.OnPatternListener { 242 243 public void onPatternStart() { 244 mLockPatternView.removeCallbacks(mCancelPatternRunnable); 245 } 246 247 public void onPatternCleared() { 248 } 249 250 public void onPatternCellAdded(List<LockPatternView.Cell> pattern) { 251 // To guard against accidental poking of the wakelock, look for 252 // the user actually trying to draw a pattern of some minimal length. 253 if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { 254 mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_MS); 255 } else { 256 // Give just a little extra time if they hit one of the first few dots 257 mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS); 258 } 259 } 260 261 public void onPatternDetected(List<LockPatternView.Cell> pattern) { 262 if (mLockPatternUtils.checkPattern(pattern)) { 263 mCallback.reportSuccessfulUnlockAttempt(); 264 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct); 265 mTotalFailedPatternAttempts = 0; 266 mCallback.dismiss(true); 267 } else { 268 if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { 269 mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_MS); 270 } 271 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); 272 if (pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) { 273 mTotalFailedPatternAttempts++; 274 mFailedPatternAttemptsSinceLastTimeout++; 275 mCallback.reportFailedUnlockAttempt(); 276 } 277 if (mFailedPatternAttemptsSinceLastTimeout 278 >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) { 279 long deadline = mLockPatternUtils.setLockoutAttemptDeadline(); 280 handleAttemptLockout(deadline); 281 } else { 282 mSecurityMessageDisplay.setMessage(R.string.kg_wrong_pattern, true); 283 mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS); 284 } 285 } 286 } 287 } 288 289 private void maybeEnableFallback(Context context) { 290 // Ask the account manager if we have an account that can be used as a 291 // fallback in case the user forgets his pattern. 292 AccountAnalyzer accountAnalyzer = new AccountAnalyzer(AccountManager.get(context)); 293 accountAnalyzer.start(); 294 } 295 296 private class AccountAnalyzer implements AccountManagerCallback<Bundle> { 297 private final AccountManager mAccountManager; 298 private final Account[] mAccounts; 299 private int mAccountIndex; 300 301 private AccountAnalyzer(AccountManager accountManager) { 302 mAccountManager = accountManager; 303 mAccounts = accountManager.getAccountsByTypeAsUser("com.google", 304 new UserHandle(mLockPatternUtils.getCurrentUser())); 305 } 306 307 private void next() { 308 // if we are ready to enable the fallback or if we depleted the list of accounts 309 // then finish and get out 310 if (mEnableFallback || mAccountIndex >= mAccounts.length) { 311 return; 312 } 313 314 // lookup the confirmCredentials intent for the current account 315 mAccountManager.confirmCredentialsAsUser(mAccounts[mAccountIndex], null, null, this, 316 null, new UserHandle(mLockPatternUtils.getCurrentUser())); 317 } 318 319 public void start() { 320 mEnableFallback = false; 321 mAccountIndex = 0; 322 next(); 323 } 324 325 public void run(AccountManagerFuture<Bundle> future) { 326 try { 327 Bundle result = future.getResult(); 328 if (result.getParcelable(AccountManager.KEY_INTENT) != null) { 329 mEnableFallback = true; 330 } 331 } catch (OperationCanceledException e) { 332 // just skip the account if we are unable to query it 333 } catch (IOException e) { 334 // just skip the account if we are unable to query it 335 } catch (AuthenticatorException e) { 336 // just skip the account if we are unable to query it 337 } finally { 338 mAccountIndex++; 339 next(); 340 } 341 } 342 } 343 344 private void handleAttemptLockout(long elapsedRealtimeDeadline) { 345 mLockPatternView.clearPattern(); 346 mLockPatternView.setEnabled(false); 347 final long elapsedRealtime = SystemClock.elapsedRealtime(); 348 if (mEnableFallback) { 349 updateFooter(FooterMode.ForgotLockPattern); 350 } 351 352 mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) { 353 354 @Override 355 public void onTick(long millisUntilFinished) { 356 final int secondsRemaining = (int) (millisUntilFinished / 1000); 357 mSecurityMessageDisplay.setMessage( 358 R.string.kg_too_many_failed_attempts_countdown, true, secondsRemaining); 359 } 360 361 @Override 362 public void onFinish() { 363 mLockPatternView.setEnabled(true); 364 displayDefaultSecurityMessage(); 365 // TODO mUnlockIcon.setVisibility(View.VISIBLE); 366 mFailedPatternAttemptsSinceLastTimeout = 0; 367 if (mEnableFallback) { 368 updateFooter(FooterMode.ForgotLockPattern); 369 } else { 370 updateFooter(FooterMode.Normal); 371 } 372 } 373 374 }.start(); 375 } 376 377 @Override 378 public boolean needsInput() { 379 return false; 380 } 381 382 @Override 383 public void onPause() { 384 if (mCountdownTimer != null) { 385 mCountdownTimer.cancel(); 386 mCountdownTimer = null; 387 } 388 } 389 390 @Override 391 public void onResume(int reason) { 392 reset(); 393 } 394 395 @Override 396 public KeyguardSecurityCallback getCallback() { 397 return mCallback; 398 } 399 400 @Override 401 public void showBouncer(int duration) { 402 KeyguardSecurityViewHelper. 403 showBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); 404 } 405 406 @Override 407 public void hideBouncer(int duration) { 408 KeyguardSecurityViewHelper. 409 hideBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); 410 } 411} 412