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.content.Context; 19import android.graphics.Rect; 20import android.os.AsyncTask; 21import android.os.CountDownTimer; 22import android.os.SystemClock; 23import android.text.TextUtils; 24import android.util.AttributeSet; 25import android.util.Log; 26import android.view.MotionEvent; 27import android.view.View; 28import android.view.ViewGroup; 29import android.view.animation.AnimationUtils; 30import android.view.animation.Interpolator; 31import android.widget.LinearLayout; 32 33import com.android.internal.widget.LockPatternChecker; 34import com.android.internal.widget.LockPatternUtils; 35import com.android.internal.widget.LockPatternView; 36import com.android.settingslib.animation.AppearAnimationCreator; 37import com.android.settingslib.animation.AppearAnimationUtils; 38import com.android.settingslib.animation.DisappearAnimationUtils; 39 40import java.util.List; 41 42public class KeyguardPatternView extends LinearLayout implements KeyguardSecurityView, 43 AppearAnimationCreator<LockPatternView.CellState>, 44 EmergencyButton.EmergencyButtonCallback { 45 46 private static final String TAG = "SecurityPatternView"; 47 private static final boolean DEBUG = KeyguardConstants.DEBUG; 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 many cells the user has to cross before we poke the wakelock 56 private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2; 57 58 private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; 59 private final AppearAnimationUtils mAppearAnimationUtils; 60 private final DisappearAnimationUtils mDisappearAnimationUtils; 61 62 private CountDownTimer mCountdownTimer = null; 63 private LockPatternUtils mLockPatternUtils; 64 private AsyncTask<?, ?, ?> mPendingLockCheck; 65 private LockPatternView mLockPatternView; 66 private KeyguardSecurityCallback mCallback; 67 68 /** 69 * Keeps track of the last time we poked the wake lock during dispatching of the touch event. 70 * Initialized to something guaranteed to make us poke the wakelock when the user starts 71 * drawing the pattern. 72 * @see #dispatchTouchEvent(android.view.MotionEvent) 73 */ 74 private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS; 75 76 /** 77 * Useful for clearing out the wrong pattern after a delay 78 */ 79 private Runnable mCancelPatternRunnable = new Runnable() { 80 @Override 81 public void run() { 82 mLockPatternView.clearPattern(); 83 } 84 }; 85 private Rect mTempRect = new Rect(); 86 private KeyguardMessageArea mSecurityMessageDisplay; 87 private View mEcaView; 88 private ViewGroup mContainer; 89 private int mDisappearYTranslation; 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 mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext); 104 mAppearAnimationUtils = new AppearAnimationUtils(context, 105 AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 1.5f /* translationScale */, 106 2.0f /* delayScale */, AnimationUtils.loadInterpolator( 107 mContext, android.R.interpolator.linear_out_slow_in)); 108 mDisappearAnimationUtils = new DisappearAnimationUtils(context, 109 125, 1.2f /* translationScale */, 110 0.6f /* delayScale */, AnimationUtils.loadInterpolator( 111 mContext, android.R.interpolator.fast_out_linear_in)); 112 mDisappearYTranslation = getResources().getDimensionPixelSize( 113 R.dimen.disappear_y_translation); 114 } 115 116 @Override 117 public void setKeyguardCallback(KeyguardSecurityCallback callback) { 118 mCallback = callback; 119 } 120 121 @Override 122 public void setLockPatternUtils(LockPatternUtils utils) { 123 mLockPatternUtils = utils; 124 } 125 126 @Override 127 protected void onFinishInflate() { 128 super.onFinishInflate(); 129 mLockPatternUtils = mLockPatternUtils == null 130 ? new LockPatternUtils(mContext) : mLockPatternUtils; 131 132 mLockPatternView = (LockPatternView) findViewById(R.id.lockPatternView); 133 mLockPatternView.setSaveEnabled(false); 134 mLockPatternView.setOnPatternListener(new UnlockPatternListener()); 135 136 // vibrate mode will be the same for the life of this screen 137 mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled()); 138 139 mSecurityMessageDisplay = 140 (KeyguardMessageArea) KeyguardMessageArea.findSecurityMessageDisplay(this); 141 mEcaView = findViewById(R.id.keyguard_selector_fade_container); 142 mContainer = (ViewGroup) findViewById(R.id.container); 143 144 EmergencyButton button = (EmergencyButton) findViewById(R.id.emergency_call_button); 145 if (button != null) { 146 button.setCallback(this); 147 } 148 } 149 150 @Override 151 public void onEmergencyButtonClickedWhenInCall() { 152 mCallback.reset(); 153 } 154 155 @Override 156 public boolean onTouchEvent(MotionEvent ev) { 157 boolean result = super.onTouchEvent(ev); 158 // as long as the user is entering a pattern (i.e sending a touch event that was handled 159 // by this screen), keep poking the wake lock so that the screen will stay on. 160 final long elapsed = SystemClock.elapsedRealtime() - mLastPokeTime; 161 if (result && (elapsed > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) { 162 mLastPokeTime = SystemClock.elapsedRealtime(); 163 } 164 mTempRect.set(0, 0, 0, 0); 165 offsetRectIntoDescendantCoords(mLockPatternView, mTempRect); 166 ev.offsetLocation(mTempRect.left, mTempRect.top); 167 result = mLockPatternView.dispatchTouchEvent(ev) || result; 168 ev.offsetLocation(-mTempRect.left, -mTempRect.top); 169 return result; 170 } 171 172 @Override 173 public void reset() { 174 // reset lock pattern 175 mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled( 176 KeyguardUpdateMonitor.getCurrentUser())); 177 mLockPatternView.enableInput(); 178 mLockPatternView.setEnabled(true); 179 mLockPatternView.clearPattern(); 180 181 // if the user is currently locked out, enforce it. 182 long deadline = mLockPatternUtils.getLockoutAttemptDeadline( 183 KeyguardUpdateMonitor.getCurrentUser()); 184 if (deadline != 0) { 185 handleAttemptLockout(deadline); 186 } else { 187 displayDefaultSecurityMessage(); 188 } 189 } 190 191 private void displayDefaultSecurityMessage() { 192 mSecurityMessageDisplay.setMessage(R.string.kg_pattern_instructions, false); 193 } 194 195 @Override 196 public void showUsabilityHint() { 197 } 198 199 /** TODO: hook this up */ 200 public void cleanUp() { 201 if (DEBUG) Log.v(TAG, "Cleanup() called on " + this); 202 mLockPatternUtils = null; 203 mLockPatternView.setOnPatternListener(null); 204 } 205 206 private class UnlockPatternListener implements LockPatternView.OnPatternListener { 207 208 @Override 209 public void onPatternStart() { 210 mLockPatternView.removeCallbacks(mCancelPatternRunnable); 211 mSecurityMessageDisplay.setMessage("", false); 212 } 213 214 @Override 215 public void onPatternCleared() { 216 } 217 218 @Override 219 public void onPatternCellAdded(List<LockPatternView.Cell> pattern) { 220 mCallback.userActivity(); 221 } 222 223 @Override 224 public void onPatternDetected(final List<LockPatternView.Cell> pattern) { 225 mLockPatternView.disableInput(); 226 if (mPendingLockCheck != null) { 227 mPendingLockCheck.cancel(false); 228 } 229 230 final int userId = KeyguardUpdateMonitor.getCurrentUser(); 231 if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) { 232 mLockPatternView.enableInput(); 233 onPatternChecked(userId, false, 0, false /* not valid - too short */); 234 return; 235 } 236 237 mPendingLockCheck = LockPatternChecker.checkPattern( 238 mLockPatternUtils, 239 pattern, 240 userId, 241 new LockPatternChecker.OnCheckCallback() { 242 @Override 243 public void onChecked(boolean matched, int timeoutMs) { 244 mLockPatternView.enableInput(); 245 mPendingLockCheck = null; 246 onPatternChecked(userId, matched, timeoutMs, true); 247 } 248 }); 249 if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { 250 mCallback.userActivity(); 251 } 252 } 253 254 private void onPatternChecked(int userId, boolean matched, int timeoutMs, 255 boolean isValidPattern) { 256 boolean dismissKeyguard = KeyguardUpdateMonitor.getCurrentUser() == userId; 257 if (matched) { 258 mCallback.reportUnlockAttempt(userId, true, 0); 259 if (dismissKeyguard) { 260 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct); 261 mCallback.dismiss(true); 262 } 263 } else { 264 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); 265 if (isValidPattern) { 266 mCallback.reportUnlockAttempt(userId, false, timeoutMs); 267 if (timeoutMs > 0) { 268 long deadline = mLockPatternUtils.setLockoutAttemptDeadline( 269 userId, timeoutMs); 270 handleAttemptLockout(deadline); 271 } 272 } 273 if (timeoutMs == 0) { 274 mSecurityMessageDisplay.setMessage(R.string.kg_wrong_pattern, true); 275 mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS); 276 } 277 } 278 } 279 } 280 281 private void handleAttemptLockout(long elapsedRealtimeDeadline) { 282 mLockPatternView.clearPattern(); 283 mLockPatternView.setEnabled(false); 284 final long elapsedRealtime = SystemClock.elapsedRealtime(); 285 286 mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) { 287 288 @Override 289 public void onTick(long millisUntilFinished) { 290 final int secondsRemaining = (int) (millisUntilFinished / 1000); 291 mSecurityMessageDisplay.setMessage( 292 R.string.kg_too_many_failed_attempts_countdown, true, secondsRemaining); 293 } 294 295 @Override 296 public void onFinish() { 297 mLockPatternView.setEnabled(true); 298 displayDefaultSecurityMessage(); 299 } 300 301 }.start(); 302 } 303 304 @Override 305 public boolean needsInput() { 306 return false; 307 } 308 309 @Override 310 public void onPause() { 311 if (mCountdownTimer != null) { 312 mCountdownTimer.cancel(); 313 mCountdownTimer = null; 314 } 315 if (mPendingLockCheck != null) { 316 mPendingLockCheck.cancel(false); 317 mPendingLockCheck = null; 318 } 319 } 320 321 @Override 322 public void onResume(int reason) { 323 reset(); 324 } 325 326 @Override 327 public KeyguardSecurityCallback getCallback() { 328 return mCallback; 329 } 330 331 @Override 332 public void showPromptReason(int reason) { 333 switch (reason) { 334 case PROMPT_REASON_RESTART: 335 mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_restart_pattern, 336 true /* important */); 337 break; 338 case PROMPT_REASON_TIMEOUT: 339 mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_timeout_pattern, 340 true /* important */); 341 break; 342 case PROMPT_REASON_DEVICE_ADMIN: 343 mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_device_admin, 344 true /* important */); 345 break; 346 case PROMPT_REASON_USER_REQUEST: 347 mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_user_request, 348 true /* important */); 349 break; 350 case PROMPT_REASON_NONE: 351 break; 352 default: 353 mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_timeout_pattern, 354 true /* important */); 355 break; 356 } 357 } 358 359 @Override 360 public void showMessage(String message, int color) { 361 mSecurityMessageDisplay.setNextMessageColor(color); 362 mSecurityMessageDisplay.setMessage(message, true /* important */); 363 } 364 365 @Override 366 public void startAppearAnimation() { 367 enableClipping(false); 368 setAlpha(1f); 369 setTranslationY(mAppearAnimationUtils.getStartTranslation()); 370 AppearAnimationUtils.startTranslationYAnimation(this, 0 /* delay */, 500 /* duration */, 371 0, mAppearAnimationUtils.getInterpolator()); 372 mAppearAnimationUtils.startAnimation2d( 373 mLockPatternView.getCellStates(), 374 new Runnable() { 375 @Override 376 public void run() { 377 enableClipping(true); 378 } 379 }, 380 this); 381 if (!TextUtils.isEmpty(mSecurityMessageDisplay.getText())) { 382 mAppearAnimationUtils.createAnimation(mSecurityMessageDisplay, 0, 383 AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 384 mAppearAnimationUtils.getStartTranslation(), 385 true /* appearing */, 386 mAppearAnimationUtils.getInterpolator(), 387 null /* finishRunnable */); 388 } 389 } 390 391 @Override 392 public boolean startDisappearAnimation(final Runnable finishRunnable) { 393 mLockPatternView.clearPattern(); 394 enableClipping(false); 395 setTranslationY(0); 396 AppearAnimationUtils.startTranslationYAnimation(this, 0 /* delay */, 300 /* duration */, 397 -mDisappearAnimationUtils.getStartTranslation(), 398 mDisappearAnimationUtils.getInterpolator()); 399 mDisappearAnimationUtils.startAnimation2d(mLockPatternView.getCellStates(), 400 new Runnable() { 401 @Override 402 public void run() { 403 enableClipping(true); 404 if (finishRunnable != null) { 405 finishRunnable.run(); 406 } 407 } 408 }, KeyguardPatternView.this); 409 if (!TextUtils.isEmpty(mSecurityMessageDisplay.getText())) { 410 mDisappearAnimationUtils.createAnimation(mSecurityMessageDisplay, 0, 411 200, 412 - mDisappearAnimationUtils.getStartTranslation() * 3, 413 false /* appearing */, 414 mDisappearAnimationUtils.getInterpolator(), 415 null /* finishRunnable */); 416 } 417 return true; 418 } 419 420 private void enableClipping(boolean enable) { 421 setClipChildren(enable); 422 mContainer.setClipToPadding(enable); 423 mContainer.setClipChildren(enable); 424 } 425 426 @Override 427 public void createAnimation(final LockPatternView.CellState animatedCell, long delay, 428 long duration, float translationY, final boolean appearing, 429 Interpolator interpolator, 430 final Runnable finishListener) { 431 mLockPatternView.startCellStateAnimation(animatedCell, 432 1f, appearing ? 1f : 0f, /* alpha */ 433 appearing ? translationY : 0f, appearing ? 0f : translationY, /* translation */ 434 appearing ? 0f : 1f, 1f /* scale */, 435 delay, duration, interpolator, finishListener); 436 if (finishListener != null) { 437 // Also animate the Emergency call 438 mAppearAnimationUtils.createAnimation(mEcaView, delay, duration, translationY, 439 appearing, interpolator, null); 440 } 441 } 442 443 @Override 444 public boolean hasOverlappingRendering() { 445 return false; 446 } 447} 448