1/* 2 * Copyright (C) 2008 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 */ 16 17package com.android.internal.policy.impl; 18 19import android.content.Context; 20import android.content.res.Configuration; 21import android.os.CountDownTimer; 22import android.os.SystemClock; 23import android.view.LayoutInflater; 24import android.view.View; 25import android.view.ViewGroup; 26import android.view.MotionEvent; 27import android.widget.Button; 28import android.widget.TextView; 29import android.text.format.DateFormat; 30import android.text.TextUtils; 31import android.util.Log; 32import com.android.internal.R; 33import com.android.internal.telephony.IccCard; 34import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient; 35import com.android.internal.widget.LockPatternUtils; 36import com.android.internal.widget.LockPatternView; 37import com.android.internal.widget.LockPatternView.Cell; 38 39import java.util.List; 40import java.util.Date; 41 42/** 43 * This is the screen that shows the 9 circle unlock widget and instructs 44 * the user how to unlock their device, or make an emergency call. 45 */ 46class PatternUnlockScreen extends LinearLayoutWithDefaultTouchRecepient 47 implements KeyguardScreen, KeyguardUpdateMonitor.InfoCallback, 48 KeyguardUpdateMonitor.SimStateCallback { 49 50 private static final boolean DEBUG = false; 51 private static final String TAG = "UnlockScreen"; 52 53 // how long before we clear the wrong pattern 54 private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000; 55 56 // how long we stay awake after each key beyond MIN_PATTERN_BEFORE_POKE_WAKELOCK 57 private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000; 58 59 // how long we stay awake after the user hits the first dot. 60 private static final int UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS = 2000; 61 62 // how many cells the user has to cross before we poke the wakelock 63 private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2; 64 65 private int mFailedPatternAttemptsSinceLastTimeout = 0; 66 private int mTotalFailedPatternAttempts = 0; 67 private CountDownTimer mCountdownTimer = null; 68 69 private final LockPatternUtils mLockPatternUtils; 70 private final KeyguardUpdateMonitor mUpdateMonitor; 71 private final KeyguardScreenCallback mCallback; 72 73 /** 74 * whether there is a fallback option available when the pattern is forgotten. 75 */ 76 private boolean mEnableFallback; 77 78 private String mDateFormatString; 79 80 private TextView mCarrier; 81 private TextView mDate; 82 83 // are we showing battery information? 84 private boolean mShowingBatteryInfo = false; 85 86 // last known plugged in state 87 private boolean mPluggedIn = false; 88 89 // last known battery level 90 private int mBatteryLevel = 100; 91 92 private String mNextAlarm = null; 93 94 private String mInstructions = null; 95 private TextView mStatus1; 96 private TextView mStatusSep; 97 private TextView mStatus2; 98 99 100 private LockPatternView mLockPatternView; 101 102 private ViewGroup mFooterNormal; 103 private ViewGroup mFooterForgotPattern; 104 105 /** 106 * Keeps track of the last time we poked the wake lock during dispatching 107 * of the touch event, initalized to something gauranteed to make us 108 * poke it when the user starts drawing the pattern. 109 * @see #dispatchTouchEvent(android.view.MotionEvent) 110 */ 111 private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS; 112 113 /** 114 * Useful for clearing out the wrong pattern after a delay 115 */ 116 private Runnable mCancelPatternRunnable = new Runnable() { 117 public void run() { 118 mLockPatternView.clearPattern(); 119 } 120 }; 121 122 private Button mForgotPatternButton; 123 private Button mEmergencyAlone; 124 private Button mEmergencyTogether; 125 private int mCreationOrientation; 126 127 enum FooterMode { 128 Normal, 129 ForgotLockPattern, 130 VerifyUnlocked 131 } 132 133 private void updateFooter(FooterMode mode) { 134 switch (mode) { 135 case Normal: 136 mFooterNormal.setVisibility(View.VISIBLE); 137 mFooterForgotPattern.setVisibility(View.GONE); 138 break; 139 case ForgotLockPattern: 140 mFooterNormal.setVisibility(View.GONE); 141 mFooterForgotPattern.setVisibility(View.VISIBLE); 142 mForgotPatternButton.setVisibility(View.VISIBLE); 143 break; 144 case VerifyUnlocked: 145 mFooterNormal.setVisibility(View.GONE); 146 mFooterForgotPattern.setVisibility(View.GONE); 147 } 148 } 149 150 /** 151 * @param context The context. 152 * @param configuration 153 * @param lockPatternUtils Used to lookup lock pattern settings. 154 * @param updateMonitor Used to lookup state affecting keyguard. 155 * @param callback Used to notify the manager when we're done, etc. 156 * @param totalFailedAttempts The current number of failed attempts. 157 * @param enableFallback True if a backup unlock option is available when the user has forgotten 158 * their pattern (e.g they have a google account so we can show them the account based 159 * backup option). 160 */ 161 PatternUnlockScreen(Context context, 162 Configuration configuration, LockPatternUtils lockPatternUtils, 163 KeyguardUpdateMonitor updateMonitor, 164 KeyguardScreenCallback callback, 165 int totalFailedAttempts) { 166 super(context); 167 mLockPatternUtils = lockPatternUtils; 168 mUpdateMonitor = updateMonitor; 169 mCallback = callback; 170 mTotalFailedPatternAttempts = totalFailedAttempts; 171 mFailedPatternAttemptsSinceLastTimeout = 172 totalFailedAttempts % LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT; 173 174 if (DEBUG) Log.d(TAG, 175 "UnlockScreen() ctor: totalFailedAttempts=" 176 + totalFailedAttempts + ", mFailedPat...=" 177 + mFailedPatternAttemptsSinceLastTimeout 178 ); 179 180 mCreationOrientation = configuration.orientation; 181 182 LayoutInflater inflater = LayoutInflater.from(context); 183 if (mCreationOrientation != Configuration.ORIENTATION_LANDSCAPE) { 184 inflater.inflate(R.layout.keyguard_screen_unlock_portrait, this, true); 185 } else { 186 inflater.inflate(R.layout.keyguard_screen_unlock_landscape, this, true); 187 } 188 189 mCarrier = (TextView) findViewById(R.id.carrier); 190 mDate = (TextView) findViewById(R.id.date); 191 192 mDateFormatString = getContext().getString(R.string.full_wday_month_day_no_year); 193 refreshTimeAndDateDisplay(); 194 195 mStatus1 = (TextView) findViewById(R.id.status1); 196 mStatusSep = (TextView) findViewById(R.id.statusSep); 197 mStatus2 = (TextView) findViewById(R.id.status2); 198 199 resetStatusInfo(); 200 201 202 mLockPatternView = (LockPatternView) findViewById(R.id.lockPattern); 203 204 mFooterNormal = (ViewGroup) findViewById(R.id.footerNormal); 205 mFooterForgotPattern = (ViewGroup) findViewById(R.id.footerForgotPattern); 206 207 // emergency call buttons 208 final OnClickListener emergencyClick = new OnClickListener() { 209 public void onClick(View v) { 210 mCallback.takeEmergencyCallAction(); 211 } 212 }; 213 214 mEmergencyAlone = (Button) findViewById(R.id.emergencyCallAlone); 215 mEmergencyAlone.setFocusable(false); // touch only! 216 mEmergencyAlone.setOnClickListener(emergencyClick); 217 mEmergencyTogether = (Button) findViewById(R.id.emergencyCallTogether); 218 mEmergencyTogether.setFocusable(false); 219 mEmergencyTogether.setOnClickListener(emergencyClick); 220 refreshEmergencyButtonText(); 221 222 mForgotPatternButton = (Button) findViewById(R.id.forgotPattern); 223 mForgotPatternButton.setText(R.string.lockscreen_forgot_pattern_button_text); 224 mForgotPatternButton.setOnClickListener(new OnClickListener() { 225 226 public void onClick(View v) { 227 mCallback.forgotPattern(true); 228 } 229 }); 230 231 // make it so unhandled touch events within the unlock screen go to the 232 // lock pattern view. 233 setDefaultTouchRecepient(mLockPatternView); 234 235 mLockPatternView.setSaveEnabled(false); 236 mLockPatternView.setFocusable(false); 237 mLockPatternView.setOnPatternListener(new UnlockPatternListener()); 238 239 // stealth mode will be the same for the life of this screen 240 mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled()); 241 242 // vibrate mode will be the same for the life of this screen 243 mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled()); 244 245 // assume normal footer mode for now 246 updateFooter(FooterMode.Normal); 247 248 updateMonitor.registerInfoCallback(this); 249 updateMonitor.registerSimStateCallback(this); 250 setFocusableInTouchMode(true); 251 252 // Required to get Marquee to work. 253 mCarrier.setSelected(true); 254 mCarrier.setTextColor(0xffffffff); 255 256 // until we get an update... 257 mCarrier.setText( 258 LockScreen.getCarrierString( 259 mUpdateMonitor.getTelephonyPlmn(), 260 mUpdateMonitor.getTelephonySpn())); 261 } 262 263 private void refreshEmergencyButtonText() { 264 mLockPatternUtils.updateEmergencyCallButtonState(mEmergencyAlone); 265 mLockPatternUtils.updateEmergencyCallButtonState(mEmergencyTogether); 266 } 267 268 public void setEnableFallback(boolean state) { 269 if (DEBUG) Log.d(TAG, "setEnableFallback(" + state + ")"); 270 mEnableFallback = state; 271 } 272 273 private void resetStatusInfo() { 274 mInstructions = null; 275 mShowingBatteryInfo = mUpdateMonitor.shouldShowBatteryInfo(); 276 mPluggedIn = mUpdateMonitor.isDevicePluggedIn(); 277 mBatteryLevel = mUpdateMonitor.getBatteryLevel(); 278 mNextAlarm = mLockPatternUtils.getNextAlarm(); 279 updateStatusLines(); 280 } 281 282 private void updateStatusLines() { 283 if (mInstructions != null) { 284 // instructions only 285 mStatus1.setText(mInstructions); 286 if (TextUtils.isEmpty(mInstructions)) { 287 mStatus1.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); 288 } else { 289 mStatus1.setCompoundDrawablesWithIntrinsicBounds( 290 R.drawable.ic_lock_idle_lock, 0, 0, 0); 291 } 292 293 mStatus1.setVisibility(View.VISIBLE); 294 mStatusSep.setVisibility(View.GONE); 295 mStatus2.setVisibility(View.GONE); 296 } else if (mShowingBatteryInfo && mNextAlarm == null) { 297 // battery only 298 if (mPluggedIn) { 299 if (mBatteryLevel >= 100) { 300 mStatus1.setText(getContext().getString(R.string.lockscreen_charged)); 301 } else { 302 mStatus1.setText(getContext().getString(R.string.lockscreen_plugged_in, mBatteryLevel)); 303 } 304 } else { 305 mStatus1.setText(getContext().getString(R.string.lockscreen_low_battery)); 306 } 307 mStatus1.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_lock_idle_charging, 0, 0, 0); 308 309 mStatus1.setVisibility(View.VISIBLE); 310 mStatusSep.setVisibility(View.GONE); 311 mStatus2.setVisibility(View.GONE); 312 313 } else if (mNextAlarm != null && !mShowingBatteryInfo) { 314 // alarm only 315 mStatus1.setText(mNextAlarm); 316 mStatus1.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_lock_idle_alarm, 0, 0, 0); 317 318 mStatus1.setVisibility(View.VISIBLE); 319 mStatusSep.setVisibility(View.GONE); 320 mStatus2.setVisibility(View.GONE); 321 } else if (mNextAlarm != null && mShowingBatteryInfo) { 322 // both battery and next alarm 323 mStatus1.setText(mNextAlarm); 324 mStatusSep.setText("|"); 325 mStatus2.setText(getContext().getString( 326 R.string.lockscreen_battery_short, 327 Math.min(100, mBatteryLevel))); 328 mStatus1.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_lock_idle_alarm, 0, 0, 0); 329 if (mPluggedIn) { 330 mStatus2.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_lock_idle_charging, 0, 0, 0); 331 } else { 332 mStatus2.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); 333 } 334 335 mStatus1.setVisibility(View.VISIBLE); 336 mStatusSep.setVisibility(View.VISIBLE); 337 mStatus2.setVisibility(View.VISIBLE); 338 } else { 339 // nothing specific to show; show general instructions 340 mStatus1.setText(R.string.lockscreen_pattern_instructions); 341 mStatus1.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_lock_idle_lock, 0, 0, 0); 342 343 mStatus1.setVisibility(View.VISIBLE); 344 mStatusSep.setVisibility(View.GONE); 345 mStatus2.setVisibility(View.GONE); 346 } 347 } 348 349 350 private void refreshTimeAndDateDisplay() { 351 mDate.setText(DateFormat.format(mDateFormatString, new Date())); 352 } 353 354 355 @Override 356 public boolean dispatchTouchEvent(MotionEvent ev) { 357 // as long as the user is entering a pattern (i.e sending a touch 358 // event that was handled by this screen), keep poking the 359 // wake lock so that the screen will stay on. 360 final boolean result = super.dispatchTouchEvent(ev); 361 if (result && 362 ((SystemClock.elapsedRealtime() - mLastPokeTime) 363 > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) { 364 mLastPokeTime = SystemClock.elapsedRealtime(); 365 } 366 return result; 367 } 368 369 370 // ---------- InfoCallback 371 372 /** {@inheritDoc} */ 373 public void onRefreshBatteryInfo(boolean showBatteryInfo, boolean pluggedIn, int batteryLevel) { 374 mShowingBatteryInfo = showBatteryInfo; 375 mPluggedIn = pluggedIn; 376 mBatteryLevel = batteryLevel; 377 updateStatusLines(); 378 } 379 380 /** {@inheritDoc} */ 381 public void onTimeChanged() { 382 refreshTimeAndDateDisplay(); 383 } 384 385 /** {@inheritDoc} */ 386 public void onRefreshCarrierInfo(CharSequence plmn, CharSequence spn) { 387 mCarrier.setText(LockScreen.getCarrierString(plmn, spn)); 388 } 389 390 /** {@inheritDoc} */ 391 public void onRingerModeChanged(int state) { 392 // not currently used 393 } 394 395 // ---------- SimStateCallback 396 397 /** {@inheritDoc} */ 398 public void onSimStateChanged(IccCard.State simState) { 399 } 400 401 @Override 402 protected void onAttachedToWindow() { 403 super.onAttachedToWindow(); 404 if (LockPatternKeyguardView.DEBUG_CONFIGURATION) { 405 Log.v(TAG, "***** PATTERN ATTACHED TO WINDOW"); 406 Log.v(TAG, "Cur orient=" + mCreationOrientation 407 + ", new config=" + getResources().getConfiguration()); 408 } 409 if (getResources().getConfiguration().orientation != mCreationOrientation) { 410 mCallback.recreateMe(getResources().getConfiguration()); 411 } 412 } 413 414 415 /** {@inheritDoc} */ 416 @Override 417 protected void onConfigurationChanged(Configuration newConfig) { 418 super.onConfigurationChanged(newConfig); 419 if (LockPatternKeyguardView.DEBUG_CONFIGURATION) { 420 Log.v(TAG, "***** PATTERN CONFIGURATION CHANGED"); 421 Log.v(TAG, "Cur orient=" + mCreationOrientation 422 + ", new config=" + getResources().getConfiguration()); 423 } 424 if (newConfig.orientation != mCreationOrientation) { 425 mCallback.recreateMe(newConfig); 426 } 427 } 428 429 /** {@inheritDoc} */ 430 public void onKeyboardChange(boolean isKeyboardOpen) {} 431 432 /** {@inheritDoc} */ 433 public boolean needsInput() { 434 return false; 435 } 436 437 /** {@inheritDoc} */ 438 public void onPause() { 439 if (mCountdownTimer != null) { 440 mCountdownTimer.cancel(); 441 mCountdownTimer = null; 442 } 443 } 444 445 /** {@inheritDoc} */ 446 public void onResume() { 447 // reset header 448 resetStatusInfo(); 449 450 // reset lock pattern 451 mLockPatternView.enableInput(); 452 mLockPatternView.setEnabled(true); 453 mLockPatternView.clearPattern(); 454 455 // show "forgot pattern?" button if we have an alternate authentication method 456 mForgotPatternButton.setVisibility(mCallback.doesFallbackUnlockScreenExist() 457 ? View.VISIBLE : View.INVISIBLE); 458 459 // if the user is currently locked out, enforce it. 460 long deadline = mLockPatternUtils.getLockoutAttemptDeadline(); 461 if (deadline != 0) { 462 handleAttemptLockout(deadline); 463 } 464 465 // the footer depends on how many total attempts the user has failed 466 if (mCallback.isVerifyUnlockOnly()) { 467 updateFooter(FooterMode.VerifyUnlocked); 468 } else if (mEnableFallback && 469 (mTotalFailedPatternAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) { 470 updateFooter(FooterMode.ForgotLockPattern); 471 } else { 472 updateFooter(FooterMode.Normal); 473 } 474 475 refreshEmergencyButtonText(); 476 } 477 478 /** {@inheritDoc} */ 479 public void cleanUp() { 480 mUpdateMonitor.removeCallback(this); 481 } 482 483 @Override 484 public void onWindowFocusChanged(boolean hasWindowFocus) { 485 super.onWindowFocusChanged(hasWindowFocus); 486 if (hasWindowFocus) { 487 // when timeout dialog closes we want to update our state 488 onResume(); 489 } 490 } 491 492 private class UnlockPatternListener 493 implements LockPatternView.OnPatternListener { 494 495 public void onPatternStart() { 496 mLockPatternView.removeCallbacks(mCancelPatternRunnable); 497 } 498 499 public void onPatternCleared() { 500 } 501 502 public void onPatternCellAdded(List<Cell> pattern) { 503 // To guard against accidental poking of the wakelock, look for 504 // the user actually trying to draw a pattern of some minimal length. 505 if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { 506 mCallback.pokeWakelock(UNLOCK_PATTERN_WAKE_INTERVAL_MS); 507 } else { 508 // Give just a little extra time if they hit one of the first few dots 509 mCallback.pokeWakelock(UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS); 510 } 511 } 512 513 public void onPatternDetected(List<LockPatternView.Cell> pattern) { 514 if (mLockPatternUtils.checkPattern(pattern)) { 515 mLockPatternView 516 .setDisplayMode(LockPatternView.DisplayMode.Correct); 517 mInstructions = ""; 518 updateStatusLines(); 519 mCallback.keyguardDone(true); 520 mCallback.reportSuccessfulUnlockAttempt(); 521 } else { 522 if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { 523 mCallback.pokeWakelock(UNLOCK_PATTERN_WAKE_INTERVAL_MS); 524 } 525 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); 526 if (pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) { 527 mTotalFailedPatternAttempts++; 528 mFailedPatternAttemptsSinceLastTimeout++; 529 mCallback.reportFailedUnlockAttempt(); 530 } 531 if (mFailedPatternAttemptsSinceLastTimeout >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) { 532 long deadline = mLockPatternUtils.setLockoutAttemptDeadline(); 533 handleAttemptLockout(deadline); 534 } else { 535 // TODO mUnlockIcon.setVisibility(View.VISIBLE); 536 mInstructions = getContext().getString(R.string.lockscreen_pattern_wrong); 537 updateStatusLines(); 538 mLockPatternView.postDelayed( 539 mCancelPatternRunnable, 540 PATTERN_CLEAR_TIMEOUT_MS); 541 } 542 } 543 } 544 } 545 546 private void handleAttemptLockout(long elapsedRealtimeDeadline) { 547 mLockPatternView.clearPattern(); 548 mLockPatternView.setEnabled(false); 549 long elapsedRealtime = SystemClock.elapsedRealtime(); 550 mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) { 551 552 @Override 553 public void onTick(long millisUntilFinished) { 554 int secondsRemaining = (int) (millisUntilFinished / 1000); 555 mInstructions = getContext().getString( 556 R.string.lockscreen_too_many_failed_attempts_countdown, 557 secondsRemaining); 558 updateStatusLines(); 559 } 560 561 @Override 562 public void onFinish() { 563 mLockPatternView.setEnabled(true); 564 mInstructions = getContext().getString(R.string.lockscreen_pattern_instructions); 565 updateStatusLines(); 566 // TODO mUnlockIcon.setVisibility(View.VISIBLE); 567 mFailedPatternAttemptsSinceLastTimeout = 0; 568 if (mEnableFallback) { 569 updateFooter(FooterMode.ForgotLockPattern); 570 } else { 571 updateFooter(FooterMode.Normal); 572 } 573 } 574 }.start(); 575 } 576 577 public void onPhoneStateChanged(String newState) { 578 refreshEmergencyButtonText(); 579 } 580} 581