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 KeyguardPatternView(Context context)94 public KeyguardPatternView(Context context) { 95 this(context, null); 96 } 97 KeyguardPatternView(Context context, AttributeSet attrs)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 setKeyguardCallback(KeyguardSecurityCallback callback)109 public void setKeyguardCallback(KeyguardSecurityCallback callback) { 110 mCallback = callback; 111 } 112 setLockPatternUtils(LockPatternUtils utils)113 public void setLockPatternUtils(LockPatternUtils utils) { 114 mLockPatternUtils = utils; 115 } 116 117 @Override onFinishInflate()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 onTouchEvent(MotionEvent ev)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 reset()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 displayDefaultSecurityMessage()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 showUsabilityHint()188 public void showUsabilityHint() { 189 } 190 191 /** TODO: hook this up */ cleanUp()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 onPatternStart()200 public void onPatternStart() { 201 mLockPatternView.removeCallbacks(mCancelPatternRunnable); 202 } 203 onPatternCleared()204 public void onPatternCleared() { 205 } 206 onPatternCellAdded(List<LockPatternView.Cell> pattern)207 public void onPatternCellAdded(List<LockPatternView.Cell> pattern) { 208 mCallback.userActivity(); 209 } 210 onPatternDetected(List<LockPatternView.Cell> pattern)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 handleAttemptLockout(long elapsedRealtimeDeadline)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 needsInput()263 public boolean needsInput() { 264 return false; 265 } 266 267 @Override onPause()268 public void onPause() { 269 if (mCountdownTimer != null) { 270 mCountdownTimer.cancel(); 271 mCountdownTimer = null; 272 } 273 } 274 275 @Override onResume(int reason)276 public void onResume(int reason) { 277 reset(); 278 } 279 280 @Override getCallback()281 public KeyguardSecurityCallback getCallback() { 282 return mCallback; 283 } 284 285 @Override showBouncer(int duration)286 public void showBouncer(int duration) { 287 KeyguardSecurityViewHelper. 288 showBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); 289 } 290 291 @Override hideBouncer(int duration)292 public void hideBouncer(int duration) { 293 KeyguardSecurityViewHelper. 294 hideBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); 295 } 296 297 @Override startAppearAnimation()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 startDisappearAnimation(Runnable finishRunnable)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 enableClipping(boolean enable)337 private void enableClipping(boolean enable) { 338 setClipChildren(enable); 339 mKeyguardBouncerFrame.setClipToPadding(enable); 340 mKeyguardBouncerFrame.setClipChildren(enable); 341 } 342 343 @Override createAnimation(final LockPatternView.CellState animatedCell, long delay, long duration, float startTranslationY, Interpolator interpolator, final Runnable finishListener)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 hasOverlappingRendering()379 public boolean hasOverlappingRendering() { 380 return false; 381 } 382 } 383