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 getIntent()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 isValidFragment(String fragmentName)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 ConfirmLockPatternFragment()105 public ConfirmLockPatternFragment() { 106 107 } 108 109 @Override onCreate(Bundle savedInstanceState)110 public void onCreate(Bundle savedInstanceState) { 111 super.onCreate(savedInstanceState); 112 } 113 114 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 onSaveInstanceState(Bundle outState)188 public void onSaveInstanceState(Bundle outState) { 189 // deliberately not calling super since we are managing this in full 190 } 191 192 @Override onPause()193 public void onPause() { 194 super.onPause(); 195 196 if (mCountdownTimer != null) { 197 mCountdownTimer.cancel(); 198 } 199 mCredentialCheckResultTracker.setListener(null); 200 } 201 202 @Override getMetricsCategory()203 public int getMetricsCategory() { 204 return MetricsEvent.CONFIRM_LOCK_PATTERN; 205 } 206 207 @Override onResume()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 onShowError()225 protected void onShowError() { 226 } 227 228 @Override prepareEnterAnimation()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 getDefaultDetails()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 getActiveViews()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 startEnterAnimation()280 public void startEnterAnimation() { 281 super.startEnterAnimation(); 282 mLockPatternView.setAlpha(1f); 283 mAppearAnimationUtils.startAnimation2d(getActiveViews(), null, this); 284 } 285 updateStage(Stage stage)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 postClearPatternRunnable()337 private void postClearPatternRunnable() { 338 mLockPatternView.removeCallbacks(mClearPatternRunnable); 339 mLockPatternView.postDelayed(mClearPatternRunnable, WRONG_PATTERN_CLEAR_TIMEOUT_MS); 340 } 341 342 @Override authenticationSucceeded()343 protected void authenticationSucceeded() { 344 mCredentialCheckResultTracker.setResult(true, new Intent(), 0, mEffectiveUserId); 345 } 346 startDisappearAnimation(final Intent intent)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 onFingerprintIconVisibilityChanged(boolean visible)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 onPatternChecked(boolean matched, Intent intent, int timeoutMs, int effectiveUserId, boolean newResult)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 onCredentialChecked(boolean matched, Intent intent, int timeoutMs, int effectiveUserId, boolean newResult)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 getLastTryErrorMessage()525 protected int getLastTryErrorMessage() { 526 return R.string.lock_profile_wipe_warning_content_pattern; 527 } 528 handleAttemptLockout(long elapsedRealtimeDeadline)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 createAnimation(Object obj, long delay, long duration, float translationY, final boolean appearing, Interpolator interpolator, final Runnable finishListener)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