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