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