ConfirmLockPattern.java revision ce25af48383b26535ec07312ab0db6928bba1fe3
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.settings; 18 19import com.android.internal.logging.MetricsLogger; 20import com.android.internal.widget.LockPatternUtils; 21import com.android.internal.widget.LockPatternView; 22import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient; 23import com.android.internal.widget.LockPatternChecker; 24import com.android.internal.widget.LockPatternView.Cell; 25import com.android.settingslib.animation.AppearAnimationCreator; 26import com.android.settingslib.animation.AppearAnimationUtils; 27import com.android.settingslib.animation.DisappearAnimationUtils; 28 29import android.animation.Animator; 30import android.animation.AnimatorListenerAdapter; 31import android.animation.ValueAnimator; 32import android.annotation.Nullable; 33import android.app.Activity; 34import android.app.Fragment; 35import android.content.Intent; 36import android.graphics.Color; 37import android.graphics.Paint; 38import android.graphics.PorterDuff; 39import android.graphics.PorterDuffColorFilter; 40import android.os.CountDownTimer; 41import android.os.SystemClock; 42import android.os.AsyncTask; 43import android.os.Bundle; 44import android.os.UserHandle; 45import android.os.storage.StorageManager; 46import android.view.MenuItem; 47import android.view.animation.AnimationUtils; 48import android.view.animation.Interpolator; 49import android.widget.TextView; 50import android.view.LayoutInflater; 51import android.view.View; 52import android.view.ViewGroup; 53 54import java.util.ArrayList; 55import java.util.Arrays; 56import java.util.Collections; 57import java.util.List; 58 59/** 60 * Launch this when you want the user to confirm their lock pattern. 61 * 62 * Sets an activity result of {@link Activity#RESULT_OK} when the user 63 * successfully confirmed their pattern. 64 */ 65public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { 66 67 public static class InternalActivity extends ConfirmLockPattern { 68 } 69 70 private enum Stage { 71 NeedToUnlock, 72 NeedToUnlockWrong, 73 LockedOut 74 } 75 76 @Override 77 public Intent getIntent() { 78 Intent modIntent = new Intent(super.getIntent()); 79 modIntent.putExtra(EXTRA_SHOW_FRAGMENT, ConfirmLockPatternFragment.class.getName()); 80 return modIntent; 81 } 82 83 @Override 84 protected boolean isValidFragment(String fragmentName) { 85 if (ConfirmLockPatternFragment.class.getName().equals(fragmentName)) return true; 86 return false; 87 } 88 89 public static class ConfirmLockPatternFragment extends ConfirmDeviceCredentialBaseFragment 90 implements AppearAnimationCreator<Object> { 91 92 // how long we wait to clear a wrong pattern 93 private static final int WRONG_PATTERN_CLEAR_TIMEOUT_MS = 2000; 94 95 private static final String KEY_NUM_WRONG_ATTEMPTS = "num_wrong_attempts"; 96 97 private LockPatternView mLockPatternView; 98 private LockPatternUtils mLockPatternUtils; 99 private AsyncTask<?, ?, ?> mPendingLockCheck; 100 private int mNumWrongConfirmAttempts; 101 private CountDownTimer mCountdownTimer; 102 103 private TextView mHeaderTextView; 104 private TextView mDetailsTextView; 105 private TextView mErrorTextView; 106 private View mLeftSpacerLandscape; 107 private View mRightSpacerLandscape; 108 109 // caller-supplied text for various prompts 110 private CharSequence mHeaderText; 111 private CharSequence mDetailsText; 112 113 private AppearAnimationUtils mAppearAnimationUtils; 114 private DisappearAnimationUtils mDisappearAnimationUtils; 115 116 // required constructor for fragments 117 public ConfirmLockPatternFragment() { 118 119 } 120 121 @Override 122 public void onCreate(Bundle savedInstanceState) { 123 super.onCreate(savedInstanceState); 124 mLockPatternUtils = new LockPatternUtils(getActivity()); 125 } 126 127 @Override 128 public View onCreateView(LayoutInflater inflater, ViewGroup container, 129 Bundle savedInstanceState) { 130 View view = inflater.inflate(R.layout.confirm_lock_pattern, null); 131 mHeaderTextView = (TextView) view.findViewById(R.id.headerText); 132 mLockPatternView = (LockPatternView) view.findViewById(R.id.lockPattern); 133 mDetailsTextView = (TextView) view.findViewById(R.id.detailsText); 134 mErrorTextView = (TextView) view.findViewById(R.id.errorText); 135 mLeftSpacerLandscape = view.findViewById(R.id.leftSpacer); 136 mRightSpacerLandscape = view.findViewById(R.id.rightSpacer); 137 138 // make it so unhandled touch events within the unlock screen go to the 139 // lock pattern view. 140 final LinearLayoutWithDefaultTouchRecepient topLayout 141 = (LinearLayoutWithDefaultTouchRecepient) view.findViewById(R.id.topLayout); 142 topLayout.setDefaultTouchRecepient(mLockPatternView); 143 144 Intent intent = getActivity().getIntent(); 145 if (intent != null) { 146 mHeaderText = intent.getCharSequenceExtra( 147 ConfirmDeviceCredentialBaseFragment.HEADER_TEXT); 148 mDetailsText = intent.getCharSequenceExtra( 149 ConfirmDeviceCredentialBaseFragment.DETAILS_TEXT); 150 } 151 152 mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled()); 153 mLockPatternView.setOnPatternListener(mConfirmExistingLockPatternListener); 154 updateStage(Stage.NeedToUnlock); 155 156 if (savedInstanceState != null) { 157 mNumWrongConfirmAttempts = savedInstanceState.getInt(KEY_NUM_WRONG_ATTEMPTS); 158 } else { 159 // on first launch, if no lock pattern is set, then finish with 160 // success (don't want user to get stuck confirming something that 161 // doesn't exist). 162 if (!mLockPatternUtils.isLockPatternEnabled(UserHandle.myUserId())) { 163 getActivity().setResult(Activity.RESULT_OK); 164 getActivity().finish(); 165 } 166 } 167 mAppearAnimationUtils = new AppearAnimationUtils(getContext(), 168 AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 2f /* translationScale */, 169 1.3f /* delayScale */, AnimationUtils.loadInterpolator( 170 getContext(), android.R.interpolator.linear_out_slow_in)); 171 mDisappearAnimationUtils = new DisappearAnimationUtils(getContext(), 172 125, 4f /* translationScale */, 173 0.3f /* delayScale */, AnimationUtils.loadInterpolator( 174 getContext(), android.R.interpolator.fast_out_linear_in), 175 new AppearAnimationUtils.RowTranslationScaler() { 176 @Override 177 public float getRowTranslationScale(int row, int numRows) { 178 return (float)(numRows - row) / numRows; 179 } 180 }); 181 setAccessibilityTitle(mHeaderTextView.getText()); 182 return view; 183 } 184 185 @Override 186 public void onSaveInstanceState(Bundle outState) { 187 // deliberately not calling super since we are managing this in full 188 outState.putInt(KEY_NUM_WRONG_ATTEMPTS, mNumWrongConfirmAttempts); 189 } 190 191 @Override 192 public void onPause() { 193 super.onPause(); 194 195 if (mCountdownTimer != null) { 196 mCountdownTimer.cancel(); 197 } 198 if (mPendingLockCheck != null) { 199 mPendingLockCheck.cancel(false); 200 mPendingLockCheck = null; 201 } 202 } 203 204 @Override 205 protected int getMetricsCategory() { 206 return MetricsLogger.CONFIRM_LOCK_PATTERN; 207 } 208 209 @Override 210 public void onResume() { 211 super.onResume(); 212 213 // if the user is currently locked out, enforce it. 214 long deadline = mLockPatternUtils.getLockoutAttemptDeadline(UserHandle.myUserId()); 215 if (deadline != 0) { 216 handleAttemptLockout(deadline); 217 } else if (!mLockPatternView.isEnabled()) { 218 // The deadline has passed, but the timer was cancelled. Or the pending lock 219 // check was cancelled. Need to clean up. 220 mNumWrongConfirmAttempts = 0; 221 updateStage(Stage.NeedToUnlock); 222 } 223 } 224 225 @Override 226 public void prepareEnterAnimation() { 227 super.prepareEnterAnimation(); 228 mHeaderTextView.setAlpha(0f); 229 mCancelButton.setAlpha(0f); 230 mLockPatternView.setAlpha(0f); 231 mDetailsTextView.setAlpha(0f); 232 mFingerprintIcon.setAlpha(0f); 233 } 234 235 private Object[][] getActiveViews() { 236 ArrayList<ArrayList<Object>> result = new ArrayList<>(); 237 result.add(new ArrayList<Object>(Collections.singletonList(mHeaderTextView))); 238 result.add(new ArrayList<Object>(Collections.singletonList(mDetailsTextView))); 239 if (mCancelButton.getVisibility() == View.VISIBLE) { 240 result.add(new ArrayList<Object>(Collections.singletonList(mCancelButton))); 241 } 242 LockPatternView.CellState[][] cellStates = mLockPatternView.getCellStates(); 243 for (int i = 0; i < cellStates.length; i++) { 244 ArrayList<Object> row = new ArrayList<>(); 245 for (int j = 0; j < cellStates[i].length; j++) { 246 row.add(cellStates[i][j]); 247 } 248 result.add(row); 249 } 250 if (mFingerprintIcon.getVisibility() == View.VISIBLE) { 251 result.add(new ArrayList<Object>(Collections.singletonList(mFingerprintIcon))); 252 } 253 Object[][] resultArr = new Object[result.size()][cellStates[0].length]; 254 for (int i = 0; i < result.size(); i++) { 255 ArrayList<Object> row = result.get(i); 256 for (int j = 0; j < row.size(); j++) { 257 resultArr[i][j] = row.get(j); 258 } 259 } 260 return resultArr; 261 } 262 263 @Override 264 public void startEnterAnimation() { 265 super.startEnterAnimation(); 266 mLockPatternView.setAlpha(1f); 267 mAppearAnimationUtils.startAnimation2d(getActiveViews(), null, this); 268 } 269 270 private void updateStage(Stage stage) { 271 switch (stage) { 272 case NeedToUnlock: 273 if (mHeaderText != null) { 274 mHeaderTextView.setText(mHeaderText); 275 } else { 276 mHeaderTextView.setText(R.string.lockpassword_confirm_your_pattern_header); 277 } 278 if (mDetailsText != null) { 279 mDetailsTextView.setText(mDetailsText); 280 } else { 281 mDetailsTextView.setText( 282 R.string.lockpassword_confirm_your_pattern_generic); 283 } 284 mErrorTextView.setText(""); 285 286 mLockPatternView.setEnabled(true); 287 mLockPatternView.enableInput(); 288 mLockPatternView.clearPattern(); 289 break; 290 case NeedToUnlockWrong: 291 mErrorTextView.setText(R.string.lockpattern_need_to_unlock_wrong); 292 293 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); 294 mLockPatternView.setEnabled(true); 295 mLockPatternView.enableInput(); 296 break; 297 case LockedOut: 298 mLockPatternView.clearPattern(); 299 // enabled = false means: disable input, and have the 300 // appearance of being disabled. 301 mLockPatternView.setEnabled(false); // appearance of being disabled 302 break; 303 } 304 305 // Always announce the header for accessibility. This is a no-op 306 // when accessibility is disabled. 307 mHeaderTextView.announceForAccessibility(mHeaderTextView.getText()); 308 } 309 310 private Runnable mClearPatternRunnable = new Runnable() { 311 public void run() { 312 mLockPatternView.clearPattern(); 313 } 314 }; 315 316 // clear the wrong pattern unless they have started a new one 317 // already 318 private void postClearPatternRunnable() { 319 mLockPatternView.removeCallbacks(mClearPatternRunnable); 320 mLockPatternView.postDelayed(mClearPatternRunnable, WRONG_PATTERN_CLEAR_TIMEOUT_MS); 321 } 322 323 @Override 324 protected void authenticationSucceeded() { 325 startDisappearAnimation(new Intent()); 326 } 327 328 private void startDisappearAnimation(final Intent intent) { 329 if (getActivity().getThemeResId() == R.style.Theme_ConfirmDeviceCredentialsDark) { 330 mLockPatternView.clearPattern(); 331 mDisappearAnimationUtils.startAnimation2d(getActiveViews(), 332 new Runnable() { 333 @Override 334 public void run() { 335 getActivity().setResult(RESULT_OK, intent); 336 getActivity().finish(); 337 getActivity().overridePendingTransition( 338 R.anim.confirm_credential_close_enter, 339 R.anim.confirm_credential_close_exit); 340 } 341 }, this); 342 } else { 343 getActivity().setResult(RESULT_OK, intent); 344 getActivity().finish(); 345 } 346 } 347 348 @Override 349 public void onFingerprintIconVisibilityChanged(boolean visible) { 350 if (mLeftSpacerLandscape != null && mRightSpacerLandscape != null) { 351 352 // In landscape, adjust spacing depending on fingerprint icon visibility. 353 mLeftSpacerLandscape.setVisibility(visible ? View.GONE : View.VISIBLE); 354 mRightSpacerLandscape.setVisibility(visible ? View.GONE : View.VISIBLE); 355 } 356 } 357 358 /** 359 * The pattern listener that responds according to a user confirming 360 * an existing lock pattern. 361 */ 362 private LockPatternView.OnPatternListener mConfirmExistingLockPatternListener 363 = new LockPatternView.OnPatternListener() { 364 365 public void onPatternStart() { 366 mLockPatternView.removeCallbacks(mClearPatternRunnable); 367 } 368 369 public void onPatternCleared() { 370 mLockPatternView.removeCallbacks(mClearPatternRunnable); 371 } 372 373 public void onPatternCellAdded(List<Cell> pattern) { 374 375 } 376 377 public void onPatternDetected(List<LockPatternView.Cell> pattern) { 378 mLockPatternView.setEnabled(false); 379 if (mPendingLockCheck != null) { 380 mPendingLockCheck.cancel(false); 381 } 382 383 final boolean verifyChallenge = getActivity().getIntent().getBooleanExtra( 384 ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, false); 385 Intent intent = new Intent(); 386 if (verifyChallenge) { 387 if (isInternalActivity()) { 388 startVerifyPattern(pattern, intent); 389 return; 390 } 391 } else { 392 startCheckPattern(pattern, intent); 393 return; 394 } 395 396 onPatternChecked(pattern, false, intent, 0); 397 } 398 399 private boolean isInternalActivity() { 400 return getActivity() instanceof ConfirmLockPattern.InternalActivity; 401 } 402 403 private void startVerifyPattern(final List<LockPatternView.Cell> pattern, 404 final Intent intent) { 405 long challenge = getActivity().getIntent().getLongExtra( 406 ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, 0); 407 mPendingLockCheck = LockPatternChecker.verifyPattern( 408 mLockPatternUtils, 409 pattern, 410 challenge, 411 UserHandle.myUserId(), 412 new LockPatternChecker.OnVerifyCallback() { 413 @Override 414 public void onVerified(byte[] token, int timeoutMs) { 415 mPendingLockCheck = null; 416 boolean matched = false; 417 if (token != null) { 418 matched = true; 419 intent.putExtra( 420 ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, 421 token); 422 } 423 onPatternChecked(pattern, matched, intent, timeoutMs); 424 } 425 }); 426 } 427 428 private void startCheckPattern(final List<LockPatternView.Cell> pattern, 429 final Intent intent) { 430 if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) { 431 onPatternChecked(pattern, false, intent, 0); 432 return; 433 } 434 435 mPendingLockCheck = LockPatternChecker.checkPattern( 436 mLockPatternUtils, 437 pattern, 438 UserHandle.myUserId(), 439 new LockPatternChecker.OnCheckCallback() { 440 @Override 441 public void onChecked(boolean matched, int timeoutMs) { 442 mPendingLockCheck = null; 443 if (matched && isInternalActivity()) { 444 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_TYPE, 445 StorageManager.CRYPT_TYPE_PATTERN); 446 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, 447 LockPatternUtils.patternToString(pattern)); 448 } 449 onPatternChecked(pattern, matched, intent, timeoutMs); 450 } 451 }); 452 } 453 454 private void onPatternChecked(List<LockPatternView.Cell> pattern, 455 boolean matched, Intent intent, int timeoutMs) { 456 mLockPatternView.setEnabled(true); 457 if (matched) { 458 startDisappearAnimation(intent); 459 } else { 460 if (timeoutMs > 0) { 461 long deadline = mLockPatternUtils.setLockoutAttemptDeadline( 462 UserHandle.myUserId(), timeoutMs); 463 handleAttemptLockout(deadline); 464 } else { 465 updateStage(Stage.NeedToUnlockWrong); 466 postClearPatternRunnable(); 467 } 468 } 469 } 470 }; 471 472 private void handleAttemptLockout(long elapsedRealtimeDeadline) { 473 updateStage(Stage.LockedOut); 474 long elapsedRealtime = SystemClock.elapsedRealtime(); 475 mCountdownTimer = new CountDownTimer( 476 elapsedRealtimeDeadline - elapsedRealtime, 477 LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS) { 478 479 @Override 480 public void onTick(long millisUntilFinished) { 481 final int secondsCountdown = (int) (millisUntilFinished / 1000); 482 mErrorTextView.setText(getString( 483 R.string.lockpattern_too_many_failed_confirmation_attempts, 484 secondsCountdown)); 485 } 486 487 @Override 488 public void onFinish() { 489 mNumWrongConfirmAttempts = 0; 490 updateStage(Stage.NeedToUnlock); 491 } 492 }.start(); 493 } 494 495 @Override 496 public void createAnimation(Object obj, long delay, 497 long duration, float translationY, final boolean appearing, 498 Interpolator interpolator, 499 final Runnable finishListener) { 500 if (obj instanceof LockPatternView.CellState) { 501 final LockPatternView.CellState animatedCell = (LockPatternView.CellState) obj; 502 if (appearing) { 503 animatedCell.scale = 0.0f; 504 animatedCell.alpha = 1.0f; 505 } 506 animatedCell.translateY = appearing ? translationY : 0; 507 ValueAnimator animator = ValueAnimator.ofFloat(animatedCell.translateY, 508 appearing ? 0 : translationY); 509 animator.setInterpolator(interpolator); 510 animator.setDuration(duration); 511 animator.setStartDelay(delay); 512 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 513 @Override 514 public void onAnimationUpdate(ValueAnimator animation) { 515 float animatedFraction = animation.getAnimatedFraction(); 516 if (appearing) { 517 animatedCell.scale = animatedFraction; 518 } else { 519 animatedCell.alpha = 1 - animatedFraction; 520 } 521 animatedCell.translateY = (float) animation.getAnimatedValue(); 522 mLockPatternView.invalidate(); 523 } 524 }); 525 if (finishListener != null) { 526 animator.addListener(new AnimatorListenerAdapter() { 527 @Override 528 public void onAnimationEnd(Animator animation) { 529 finishListener.run(); 530 } 531 }); 532 } 533 534 animator.start(); 535 mLockPatternView.invalidate(); 536 } else { 537 mAppearAnimationUtils.createAnimation((View) obj, delay, duration, translationY, 538 appearing, interpolator, finishListener); 539 } 540 } 541 } 542} 543