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