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 */ 16 package com.android.keyguard; 17 18 import android.content.Context; 19 import android.graphics.Rect; 20 import android.os.AsyncTask; 21 import android.os.CountDownTimer; 22 import android.os.SystemClock; 23 import android.text.TextUtils; 24 import android.util.AttributeSet; 25 import android.util.Log; 26 import android.view.MotionEvent; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.view.animation.AnimationUtils; 30 import android.view.animation.Interpolator; 31 import android.widget.LinearLayout; 32 33 import com.android.internal.widget.LockPatternChecker; 34 import com.android.internal.widget.LockPatternUtils; 35 import com.android.internal.widget.LockPatternView; 36 import com.android.settingslib.animation.AppearAnimationCreator; 37 import com.android.settingslib.animation.AppearAnimationUtils; 38 import com.android.settingslib.animation.DisappearAnimationUtils; 39 40 import java.util.List; 41 42 public 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 KeyguardPatternView(Context context)97 public KeyguardPatternView(Context context) { 98 this(context, null); 99 } 100 KeyguardPatternView(Context context, AttributeSet attrs)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 setKeyguardCallback(KeyguardSecurityCallback callback)117 public void setKeyguardCallback(KeyguardSecurityCallback callback) { 118 mCallback = callback; 119 } 120 121 @Override setLockPatternUtils(LockPatternUtils utils)122 public void setLockPatternUtils(LockPatternUtils utils) { 123 mLockPatternUtils = utils; 124 } 125 126 @Override onFinishInflate()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 // stealth mode will be the same for the life of this screen 137 mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled( 138 KeyguardUpdateMonitor.getCurrentUser())); 139 140 // vibrate mode will be the same for the life of this screen 141 mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled()); 142 143 mSecurityMessageDisplay = 144 (KeyguardMessageArea) KeyguardMessageArea.findSecurityMessageDisplay(this); 145 mEcaView = findViewById(R.id.keyguard_selector_fade_container); 146 mContainer = (ViewGroup) findViewById(R.id.container); 147 148 EmergencyButton button = (EmergencyButton) findViewById(R.id.emergency_call_button); 149 if (button != null) { 150 button.setCallback(this); 151 } 152 } 153 154 @Override onEmergencyButtonClickedWhenInCall()155 public void onEmergencyButtonClickedWhenInCall() { 156 mCallback.reset(); 157 } 158 159 @Override onTouchEvent(MotionEvent ev)160 public boolean onTouchEvent(MotionEvent ev) { 161 boolean result = super.onTouchEvent(ev); 162 // as long as the user is entering a pattern (i.e sending a touch event that was handled 163 // by this screen), keep poking the wake lock so that the screen will stay on. 164 final long elapsed = SystemClock.elapsedRealtime() - mLastPokeTime; 165 if (result && (elapsed > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) { 166 mLastPokeTime = SystemClock.elapsedRealtime(); 167 } 168 mTempRect.set(0, 0, 0, 0); 169 offsetRectIntoDescendantCoords(mLockPatternView, mTempRect); 170 ev.offsetLocation(mTempRect.left, mTempRect.top); 171 result = mLockPatternView.dispatchTouchEvent(ev) || result; 172 ev.offsetLocation(-mTempRect.left, -mTempRect.top); 173 return result; 174 } 175 176 @Override reset()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 KeyguardUpdateMonitor.getCurrentUser()); 186 if (deadline != 0) { 187 handleAttemptLockout(deadline); 188 } else { 189 displayDefaultSecurityMessage(); 190 } 191 } 192 displayDefaultSecurityMessage()193 private void displayDefaultSecurityMessage() { 194 mSecurityMessageDisplay.setMessage(R.string.kg_pattern_instructions, false); 195 } 196 197 @Override showUsabilityHint()198 public void showUsabilityHint() { 199 } 200 201 /** TODO: hook this up */ cleanUp()202 public void cleanUp() { 203 if (DEBUG) Log.v(TAG, "Cleanup() called on " + this); 204 mLockPatternUtils = null; 205 mLockPatternView.setOnPatternListener(null); 206 } 207 208 private class UnlockPatternListener implements LockPatternView.OnPatternListener { 209 210 @Override onPatternStart()211 public void onPatternStart() { 212 mLockPatternView.removeCallbacks(mCancelPatternRunnable); 213 mSecurityMessageDisplay.setMessage("", false); 214 } 215 216 @Override onPatternCleared()217 public void onPatternCleared() { 218 } 219 220 @Override onPatternCellAdded(List<LockPatternView.Cell> pattern)221 public void onPatternCellAdded(List<LockPatternView.Cell> pattern) { 222 mCallback.userActivity(); 223 } 224 225 @Override onPatternDetected(final List<LockPatternView.Cell> pattern)226 public void onPatternDetected(final List<LockPatternView.Cell> pattern) { 227 mLockPatternView.disableInput(); 228 if (mPendingLockCheck != null) { 229 mPendingLockCheck.cancel(false); 230 } 231 232 if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) { 233 mLockPatternView.enableInput(); 234 onPatternChecked(false, 0, false /* not valid - too short */); 235 return; 236 } 237 238 mPendingLockCheck = LockPatternChecker.checkPattern( 239 mLockPatternUtils, 240 pattern, 241 KeyguardUpdateMonitor.getCurrentUser(), 242 new LockPatternChecker.OnCheckCallback() { 243 @Override 244 public void onChecked(boolean matched, int timeoutMs) { 245 mLockPatternView.enableInput(); 246 mPendingLockCheck = null; 247 onPatternChecked(matched, timeoutMs, true); 248 } 249 }); 250 if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { 251 mCallback.userActivity(); 252 } 253 } 254 onPatternChecked(boolean matched, int timeoutMs, boolean isValidPattern)255 private void onPatternChecked(boolean matched, int timeoutMs, boolean isValidPattern) { 256 if (matched) { 257 mCallback.reportUnlockAttempt(true, 0); 258 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct); 259 mCallback.dismiss(true); 260 } else { 261 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); 262 if (isValidPattern) { 263 mCallback.reportUnlockAttempt(false, timeoutMs); 264 if (timeoutMs > 0) { 265 long deadline = mLockPatternUtils.setLockoutAttemptDeadline( 266 KeyguardUpdateMonitor.getCurrentUser(), timeoutMs); 267 handleAttemptLockout(deadline); 268 } 269 } 270 if (timeoutMs == 0) { 271 mSecurityMessageDisplay.setMessage(R.string.kg_wrong_pattern, true); 272 mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS); 273 } 274 } 275 } 276 } 277 handleAttemptLockout(long elapsedRealtimeDeadline)278 private void handleAttemptLockout(long elapsedRealtimeDeadline) { 279 mLockPatternView.clearPattern(); 280 mLockPatternView.setEnabled(false); 281 final long elapsedRealtime = SystemClock.elapsedRealtime(); 282 283 mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) { 284 285 @Override 286 public void onTick(long millisUntilFinished) { 287 final int secondsRemaining = (int) (millisUntilFinished / 1000); 288 mSecurityMessageDisplay.setMessage( 289 R.string.kg_too_many_failed_attempts_countdown, true, secondsRemaining); 290 } 291 292 @Override 293 public void onFinish() { 294 mLockPatternView.setEnabled(true); 295 displayDefaultSecurityMessage(); 296 } 297 298 }.start(); 299 } 300 301 @Override needsInput()302 public boolean needsInput() { 303 return false; 304 } 305 306 @Override onPause()307 public void onPause() { 308 if (mCountdownTimer != null) { 309 mCountdownTimer.cancel(); 310 mCountdownTimer = null; 311 } 312 if (mPendingLockCheck != null) { 313 mPendingLockCheck.cancel(false); 314 mPendingLockCheck = null; 315 } 316 } 317 318 @Override onResume(int reason)319 public void onResume(int reason) { 320 reset(); 321 } 322 323 @Override getCallback()324 public KeyguardSecurityCallback getCallback() { 325 return mCallback; 326 } 327 328 @Override showPromptReason(int reason)329 public void showPromptReason(int reason) { 330 switch (reason) { 331 case PROMPT_REASON_RESTART: 332 mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_restart_pattern, 333 true /* important */); 334 break; 335 case PROMPT_REASON_TIMEOUT: 336 mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_timeout_pattern, 337 true /* important */); 338 break; 339 default: 340 break; 341 } 342 } 343 344 @Override showMessage(String message, int color)345 public void showMessage(String message, int color) { 346 mSecurityMessageDisplay.setNextMessageColor(color); 347 mSecurityMessageDisplay.setMessage(message, true /* important */); 348 } 349 350 @Override startAppearAnimation()351 public void startAppearAnimation() { 352 enableClipping(false); 353 setAlpha(1f); 354 setTranslationY(mAppearAnimationUtils.getStartTranslation()); 355 AppearAnimationUtils.startTranslationYAnimation(this, 0 /* delay */, 500 /* duration */, 356 0, mAppearAnimationUtils.getInterpolator()); 357 mAppearAnimationUtils.startAnimation2d( 358 mLockPatternView.getCellStates(), 359 new Runnable() { 360 @Override 361 public void run() { 362 enableClipping(true); 363 } 364 }, 365 this); 366 if (!TextUtils.isEmpty(mSecurityMessageDisplay.getText())) { 367 mAppearAnimationUtils.createAnimation(mSecurityMessageDisplay, 0, 368 AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 369 mAppearAnimationUtils.getStartTranslation(), 370 true /* appearing */, 371 mAppearAnimationUtils.getInterpolator(), 372 null /* finishRunnable */); 373 } 374 } 375 376 @Override startDisappearAnimation(final Runnable finishRunnable)377 public boolean startDisappearAnimation(final Runnable finishRunnable) { 378 mLockPatternView.clearPattern(); 379 enableClipping(false); 380 setTranslationY(0); 381 AppearAnimationUtils.startTranslationYAnimation(this, 0 /* delay */, 300 /* duration */, 382 -mDisappearAnimationUtils.getStartTranslation(), 383 mDisappearAnimationUtils.getInterpolator()); 384 mDisappearAnimationUtils.startAnimation2d(mLockPatternView.getCellStates(), 385 new Runnable() { 386 @Override 387 public void run() { 388 enableClipping(true); 389 if (finishRunnable != null) { 390 finishRunnable.run(); 391 } 392 } 393 }, KeyguardPatternView.this); 394 if (!TextUtils.isEmpty(mSecurityMessageDisplay.getText())) { 395 mDisappearAnimationUtils.createAnimation(mSecurityMessageDisplay, 0, 396 200, 397 - mDisappearAnimationUtils.getStartTranslation() * 3, 398 false /* appearing */, 399 mDisappearAnimationUtils.getInterpolator(), 400 null /* finishRunnable */); 401 } 402 return true; 403 } 404 enableClipping(boolean enable)405 private void enableClipping(boolean enable) { 406 setClipChildren(enable); 407 mContainer.setClipToPadding(enable); 408 mContainer.setClipChildren(enable); 409 } 410 411 @Override createAnimation(final LockPatternView.CellState animatedCell, long delay, long duration, float translationY, final boolean appearing, Interpolator interpolator, final Runnable finishListener)412 public void createAnimation(final LockPatternView.CellState animatedCell, long delay, 413 long duration, float translationY, final boolean appearing, 414 Interpolator interpolator, 415 final Runnable finishListener) { 416 mLockPatternView.startCellStateAnimation(animatedCell, 417 1f, appearing ? 1f : 0f, /* alpha */ 418 appearing ? translationY : 0f, appearing ? 0f : translationY, /* translation */ 419 appearing ? 0f : 1f, 1f /* scale */, 420 delay, duration, interpolator, finishListener); 421 if (finishListener != null) { 422 // Also animate the Emergency call 423 mAppearAnimationUtils.createAnimation(mEcaView, delay, duration, translationY, 424 appearing, interpolator, null); 425 } 426 } 427 428 @Override hasOverlappingRendering()429 public boolean hasOverlappingRendering() { 430 return false; 431 } 432 } 433