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.animation.Animator; 19 import android.animation.AnimatorListenerAdapter; 20 import android.animation.ValueAnimator; 21 import android.content.Context; 22 import android.graphics.Rect; 23 import android.graphics.drawable.Drawable; 24 import android.os.CountDownTimer; 25 import android.os.SystemClock; 26 import android.text.TextUtils; 27 import android.util.AttributeSet; 28 import android.util.Log; 29 import android.view.MotionEvent; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.view.animation.AnimationUtils; 33 import android.view.animation.Interpolator; 34 import android.widget.LinearLayout; 35 36 import com.android.internal.widget.LockPatternUtils; 37 import com.android.internal.widget.LockPatternView; 38 39 import java.util.List; 40 41 public class KeyguardPatternView extends LinearLayout implements KeyguardSecurityView, 42 AppearAnimationCreator<LockPatternView.CellState> { 43 44 private static final String TAG = "SecurityPatternView"; 45 private static final boolean DEBUG = KeyguardConstants.DEBUG; 46 47 // how long before we clear the wrong pattern 48 private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000; 49 50 // how long we stay awake after each key beyond MIN_PATTERN_BEFORE_POKE_WAKELOCK 51 private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000; 52 53 // how many cells the user has to cross before we poke the wakelock 54 private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2; 55 56 private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; 57 private final AppearAnimationUtils mAppearAnimationUtils; 58 59 private CountDownTimer mCountdownTimer = null; 60 private LockPatternUtils mLockPatternUtils; 61 private LockPatternView mLockPatternView; 62 private KeyguardSecurityCallback mCallback; 63 64 /** 65 * Keeps track of the last time we poked the wake lock during dispatching of the touch event. 66 * Initialized to something guaranteed to make us poke the wakelock when the user starts 67 * drawing the pattern. 68 * @see #dispatchTouchEvent(android.view.MotionEvent) 69 */ 70 private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS; 71 72 /** 73 * Useful for clearing out the wrong pattern after a delay 74 */ 75 private Runnable mCancelPatternRunnable = new Runnable() { 76 public void run() { 77 mLockPatternView.clearPattern(); 78 } 79 }; 80 private Rect mTempRect = new Rect(); 81 private SecurityMessageDisplay mSecurityMessageDisplay; 82 private View mEcaView; 83 private Drawable mBouncerFrame; 84 private ViewGroup mKeyguardBouncerFrame; 85 private KeyguardMessageArea mHelpMessage; 86 private int mDisappearYTranslation; 87 88 enum FooterMode { 89 Normal, 90 ForgotLockPattern, 91 VerifyUnlocked 92 } 93 94 public KeyguardPatternView(Context context) { 95 this(context, null); 96 } 97 98 public KeyguardPatternView(Context context, AttributeSet attrs) { 99 super(context, attrs); 100 mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext); 101 mAppearAnimationUtils = new AppearAnimationUtils(context, 102 AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 1.5f /* delayScale */, 103 2.0f /* transitionScale */, AnimationUtils.loadInterpolator( 104 mContext, android.R.interpolator.linear_out_slow_in)); 105 mDisappearYTranslation = getResources().getDimensionPixelSize( 106 R.dimen.disappear_y_translation); 107 } 108 109 public void setKeyguardCallback(KeyguardSecurityCallback callback) { 110 mCallback = callback; 111 } 112 113 public void setLockPatternUtils(LockPatternUtils utils) { 114 mLockPatternUtils = utils; 115 } 116 117 @Override 118 protected void onFinishInflate() { 119 super.onFinishInflate(); 120 mLockPatternUtils = mLockPatternUtils == null 121 ? new LockPatternUtils(mContext) : mLockPatternUtils; 122 123 mLockPatternView = (LockPatternView) findViewById(R.id.lockPatternView); 124 mLockPatternView.setSaveEnabled(false); 125 mLockPatternView.setFocusable(false); 126 mLockPatternView.setOnPatternListener(new UnlockPatternListener()); 127 128 // stealth mode will be the same for the life of this screen 129 mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled()); 130 131 // vibrate mode will be the same for the life of this screen 132 mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled()); 133 134 setFocusableInTouchMode(true); 135 136 mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this); 137 mEcaView = findViewById(R.id.keyguard_selector_fade_container); 138 View bouncerFrameView = findViewById(R.id.keyguard_bouncer_frame); 139 if (bouncerFrameView != null) { 140 mBouncerFrame = bouncerFrameView.getBackground(); 141 } 142 143 mKeyguardBouncerFrame = (ViewGroup) findViewById(R.id.keyguard_bouncer_frame); 144 mHelpMessage = (KeyguardMessageArea) findViewById(R.id.keyguard_message_area); 145 } 146 147 @Override 148 public boolean onTouchEvent(MotionEvent ev) { 149 boolean result = super.onTouchEvent(ev); 150 // as long as the user is entering a pattern (i.e sending a touch event that was handled 151 // by this screen), keep poking the wake lock so that the screen will stay on. 152 final long elapsed = SystemClock.elapsedRealtime() - mLastPokeTime; 153 if (result && (elapsed > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) { 154 mLastPokeTime = SystemClock.elapsedRealtime(); 155 } 156 mTempRect.set(0, 0, 0, 0); 157 offsetRectIntoDescendantCoords(mLockPatternView, mTempRect); 158 ev.offsetLocation(mTempRect.left, mTempRect.top); 159 result = mLockPatternView.dispatchTouchEvent(ev) || result; 160 ev.offsetLocation(-mTempRect.left, -mTempRect.top); 161 return result; 162 } 163 164 public void reset() { 165 // reset lock pattern 166 mLockPatternView.enableInput(); 167 mLockPatternView.setEnabled(true); 168 mLockPatternView.clearPattern(); 169 170 // if the user is currently locked out, enforce it. 171 long deadline = mLockPatternUtils.getLockoutAttemptDeadline(); 172 if (deadline != 0) { 173 handleAttemptLockout(deadline); 174 } else { 175 displayDefaultSecurityMessage(); 176 } 177 } 178 179 private void displayDefaultSecurityMessage() { 180 if (mKeyguardUpdateMonitor.getMaxBiometricUnlockAttemptsReached()) { 181 mSecurityMessageDisplay.setMessage(R.string.faceunlock_multiple_failures, true); 182 } else { 183 mSecurityMessageDisplay.setMessage(R.string.kg_pattern_instructions, false); 184 } 185 } 186 187 @Override 188 public void showUsabilityHint() { 189 } 190 191 /** TODO: hook this up */ 192 public void cleanUp() { 193 if (DEBUG) Log.v(TAG, "Cleanup() called on " + this); 194 mLockPatternUtils = null; 195 mLockPatternView.setOnPatternListener(null); 196 } 197 198 private class UnlockPatternListener implements LockPatternView.OnPatternListener { 199 200 public void onPatternStart() { 201 mLockPatternView.removeCallbacks(mCancelPatternRunnable); 202 } 203 204 public void onPatternCleared() { 205 } 206 207 public void onPatternCellAdded(List<LockPatternView.Cell> pattern) { 208 mCallback.userActivity(); 209 } 210 211 public void onPatternDetected(List<LockPatternView.Cell> pattern) { 212 if (mLockPatternUtils.checkPattern(pattern)) { 213 mCallback.reportUnlockAttempt(true); 214 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct); 215 mCallback.dismiss(true); 216 } else { 217 if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { 218 mCallback.userActivity(); 219 } 220 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); 221 boolean registeredAttempt = 222 pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL; 223 if (registeredAttempt) { 224 mCallback.reportUnlockAttempt(false); 225 } 226 int attempts = mKeyguardUpdateMonitor.getFailedUnlockAttempts(); 227 if (registeredAttempt && 228 0 == (attempts % LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) { 229 long deadline = mLockPatternUtils.setLockoutAttemptDeadline(); 230 handleAttemptLockout(deadline); 231 } else { 232 mSecurityMessageDisplay.setMessage(R.string.kg_wrong_pattern, true); 233 mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS); 234 } 235 } 236 } 237 } 238 239 private void handleAttemptLockout(long elapsedRealtimeDeadline) { 240 mLockPatternView.clearPattern(); 241 mLockPatternView.setEnabled(false); 242 final long elapsedRealtime = SystemClock.elapsedRealtime(); 243 244 mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) { 245 246 @Override 247 public void onTick(long millisUntilFinished) { 248 final int secondsRemaining = (int) (millisUntilFinished / 1000); 249 mSecurityMessageDisplay.setMessage( 250 R.string.kg_too_many_failed_attempts_countdown, true, secondsRemaining); 251 } 252 253 @Override 254 public void onFinish() { 255 mLockPatternView.setEnabled(true); 256 displayDefaultSecurityMessage(); 257 } 258 259 }.start(); 260 } 261 262 @Override 263 public boolean needsInput() { 264 return false; 265 } 266 267 @Override 268 public void onPause() { 269 if (mCountdownTimer != null) { 270 mCountdownTimer.cancel(); 271 mCountdownTimer = null; 272 } 273 } 274 275 @Override 276 public void onResume(int reason) { 277 reset(); 278 } 279 280 @Override 281 public KeyguardSecurityCallback getCallback() { 282 return mCallback; 283 } 284 285 @Override 286 public void showBouncer(int duration) { 287 KeyguardSecurityViewHelper. 288 showBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); 289 } 290 291 @Override 292 public void hideBouncer(int duration) { 293 KeyguardSecurityViewHelper. 294 hideBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); 295 } 296 297 @Override 298 public void startAppearAnimation() { 299 enableClipping(false); 300 setAlpha(1f); 301 setTranslationY(mAppearAnimationUtils.getStartTranslation()); 302 animate() 303 .setDuration(500) 304 .setInterpolator(mAppearAnimationUtils.getInterpolator()) 305 .translationY(0); 306 mAppearAnimationUtils.startAppearAnimation( 307 mLockPatternView.getCellStates(), 308 new Runnable() { 309 @Override 310 public void run() { 311 enableClipping(true); 312 } 313 }, 314 this); 315 if (!TextUtils.isEmpty(mHelpMessage.getText())) { 316 mAppearAnimationUtils.createAnimation(mHelpMessage, 0, 317 AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 318 mAppearAnimationUtils.getStartTranslation(), 319 mAppearAnimationUtils.getInterpolator(), 320 null /* finishRunnable */); 321 } 322 } 323 324 @Override 325 public boolean startDisappearAnimation(Runnable finishRunnable) { 326 mLockPatternView.clearPattern(); 327 animate() 328 .alpha(0f) 329 .translationY(mDisappearYTranslation) 330 .setInterpolator(AnimationUtils.loadInterpolator( 331 mContext, android.R.interpolator.fast_out_linear_in)) 332 .setDuration(100) 333 .withEndAction(finishRunnable); 334 return true; 335 } 336 337 private void enableClipping(boolean enable) { 338 setClipChildren(enable); 339 mKeyguardBouncerFrame.setClipToPadding(enable); 340 mKeyguardBouncerFrame.setClipChildren(enable); 341 } 342 343 @Override 344 public void createAnimation(final LockPatternView.CellState animatedCell, long delay, 345 long duration, float startTranslationY, Interpolator interpolator, 346 final Runnable finishListener) { 347 animatedCell.scale = 0.0f; 348 animatedCell.translateY = startTranslationY; 349 ValueAnimator animator = ValueAnimator.ofFloat(startTranslationY, 0.0f); 350 animator.setInterpolator(interpolator); 351 animator.setDuration(duration); 352 animator.setStartDelay(delay); 353 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 354 @Override 355 public void onAnimationUpdate(ValueAnimator animation) { 356 float animatedFraction = animation.getAnimatedFraction(); 357 animatedCell.scale = animatedFraction; 358 animatedCell.translateY = (float) animation.getAnimatedValue(); 359 mLockPatternView.invalidate(); 360 } 361 }); 362 if (finishListener != null) { 363 animator.addListener(new AnimatorListenerAdapter() { 364 @Override 365 public void onAnimationEnd(Animator animation) { 366 finishListener.run(); 367 } 368 }); 369 370 // Also animate the Emergency call 371 mAppearAnimationUtils.createAnimation(mEcaView, delay, duration, startTranslationY, 372 interpolator, null); 373 } 374 animator.start(); 375 mLockPatternView.invalidate(); 376 } 377 378 @Override 379 public boolean hasOverlappingRendering() { 380 return false; 381 } 382 } 383