1 /* 2 * Copyright (C) 2018 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.car.settings.security; 18 19 import android.os.Bundle; 20 import android.os.UserHandle; 21 import android.view.View; 22 import android.widget.TextView; 23 24 import androidx.annotation.LayoutRes; 25 import androidx.annotation.StringRes; 26 import androidx.annotation.VisibleForTesting; 27 28 import com.android.car.settings.R; 29 import com.android.car.settings.common.BaseFragment; 30 import com.android.car.settings.common.Logger; 31 import com.android.car.ui.toolbar.MenuItem; 32 import com.android.internal.widget.LockPatternUtils; 33 import com.android.internal.widget.LockPatternView; 34 import com.android.internal.widget.LockPatternView.Cell; 35 import com.android.internal.widget.LockPatternView.DisplayMode; 36 import com.android.internal.widget.LockscreenCredential; 37 38 import com.google.android.collect.Lists; 39 40 import java.util.Arrays; 41 import java.util.Collections; 42 import java.util.List; 43 44 /** 45 * Fragment for choosing security lock pattern. 46 */ 47 public class ChooseLockPatternFragment extends BaseFragment { 48 49 private static final Logger LOG = new Logger(ChooseLockPatternFragment.class); 50 private static final String FRAGMENT_TAG_SAVE_PATTERN_WORKER = "save_pattern_worker"; 51 private static final String STATE_UI_STAGE = "state_ui_stage"; 52 private static final String STATE_CHOSEN_PATTERN = "state_chosen_pattern"; 53 private static final int ID_EMPTY_MESSAGE = -1; 54 /** 55 * The patten used during the help screen to show how to draw a pattern. 56 */ 57 private final List<LockPatternView.Cell> mAnimatePattern = 58 Collections.unmodifiableList(Lists.newArrayList( 59 LockPatternView.Cell.of(0, 0), 60 LockPatternView.Cell.of(0, 1), 61 LockPatternView.Cell.of(1, 1), 62 LockPatternView.Cell.of(2, 1) 63 )); 64 // How long we wait to clear a wrong pattern 65 private int mWrongPatternClearTimeOut; 66 private int mUserId; 67 private Stage mUiStage = Stage.Introduction; 68 private LockPatternView mLockPatternView; 69 private TextView mMessageText; 70 private LockscreenCredential mChosenPattern; 71 private MenuItem mSecondaryButton; 72 private MenuItem mPrimaryButton; 73 // Existing pattern that user previously set 74 private LockscreenCredential mCurrentCredential; 75 private SaveLockWorker mSaveLockWorker; 76 private Runnable mClearPatternRunnable = () -> mLockPatternView.clearPattern(); 77 // The pattern listener that responds according to a user choosing a new 78 // lock pattern. 79 private final LockPatternView.OnPatternListener mChooseNewLockPatternListener = 80 new LockPatternView.OnPatternListener() { 81 @Override 82 public void onPatternStart() { 83 mLockPatternView.removeCallbacks(mClearPatternRunnable); 84 updateUIWhenPatternInProgress(); 85 } 86 87 @Override 88 public void onPatternCleared() { 89 mLockPatternView.removeCallbacks(mClearPatternRunnable); 90 } 91 92 @Override 93 public void onPatternDetected(List<LockPatternView.Cell> pattern) { 94 switch (mUiStage) { 95 case Introduction: 96 case ChoiceTooShort: 97 handlePatternEntered(pattern); 98 break; 99 case ConfirmWrong: 100 case NeedToConfirm: 101 handleConfirmPattern(pattern); 102 break; 103 default: 104 throw new IllegalStateException("Unexpected stage " + mUiStage 105 + " when entering the pattern."); 106 } 107 } 108 109 @Override 110 public void onPatternCellAdded(List<Cell> pattern) { 111 } 112 113 private void handleConfirmPattern(List<LockPatternView.Cell> pattern) { 114 if (mChosenPattern == null) { 115 throw new IllegalStateException( 116 "null chosen pattern in stage 'need to confirm"); 117 } 118 try (LockscreenCredential credential = 119 LockscreenCredential.createPattern(pattern)) { 120 if (mChosenPattern.equals(credential)) { 121 updateStage(Stage.ChoiceConfirmed); 122 } else { 123 updateStage(Stage.ConfirmWrong); 124 } 125 } 126 } 127 128 private void handlePatternEntered(List<LockPatternView.Cell> pattern) { 129 if (pattern.size() < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) { 130 updateStage(Stage.ChoiceTooShort); 131 } else { 132 mChosenPattern = LockscreenCredential.createPattern(pattern); 133 updateStage(Stage.FirstChoiceValid); 134 } 135 } 136 }; 137 138 /** 139 * Factory method for creating ChooseLockPatternFragment 140 */ newInstance()141 public static ChooseLockPatternFragment newInstance() { 142 ChooseLockPatternFragment patternFragment = new ChooseLockPatternFragment(); 143 return patternFragment; 144 } 145 146 @Override getToolbarMenuItems()147 public List<MenuItem> getToolbarMenuItems() { 148 return Arrays.asList(mPrimaryButton, mSecondaryButton); 149 } 150 151 @Override 152 @LayoutRes getLayoutId()153 protected int getLayoutId() { 154 return R.layout.choose_lock_pattern; 155 } 156 157 @Override 158 @StringRes getTitleId()159 protected int getTitleId() { 160 return R.string.security_lock_pattern; 161 } 162 163 @Override onCreate(Bundle savedInstanceState)164 public void onCreate(Bundle savedInstanceState) { 165 super.onCreate(savedInstanceState); 166 mWrongPatternClearTimeOut = getResources().getInteger(R.integer.clear_content_timeout_ms); 167 mUserId = UserHandle.myUserId(); 168 169 Bundle args = getArguments(); 170 if (args != null) { 171 mCurrentCredential = args.getParcelable(PasswordHelper.EXTRA_CURRENT_SCREEN_LOCK); 172 if (mCurrentCredential != null) { 173 mCurrentCredential = mCurrentCredential.duplicate(); 174 } 175 } 176 177 if (savedInstanceState != null) { 178 mUiStage = Stage.values()[savedInstanceState.getInt(STATE_UI_STAGE)]; 179 mChosenPattern = savedInstanceState.getParcelable(STATE_CHOSEN_PATTERN); 180 } 181 182 mPrimaryButton = new MenuItem.Builder(getContext()) 183 .setOnClickListener(i -> handlePrimaryButtonClick()) 184 .build(); 185 mSecondaryButton = new MenuItem.Builder(getContext()) 186 .setOnClickListener(i -> handleSecondaryButtonClick()) 187 .build(); 188 } 189 190 @Override onViewCreated(View view, Bundle savedInstanceState)191 public void onViewCreated(View view, Bundle savedInstanceState) { 192 super.onViewCreated(view, savedInstanceState); 193 194 mMessageText = view.findViewById(R.id.description_text); 195 mMessageText.setText(getString(R.string.choose_lock_pattern_message)); 196 197 mLockPatternView = view.findViewById(R.id.lockPattern); 198 mLockPatternView.setVisibility(View.VISIBLE); 199 mLockPatternView.setEnabled(true); 200 mLockPatternView.setFadePattern(false); 201 mLockPatternView.clearPattern(); 202 mLockPatternView.setOnPatternListener(mChooseNewLockPatternListener); 203 204 // Re-attach to the exiting worker if there is one. 205 if (savedInstanceState != null) { 206 mSaveLockWorker = (SaveLockWorker) getFragmentManager().findFragmentByTag( 207 FRAGMENT_TAG_SAVE_PATTERN_WORKER); 208 } 209 } 210 211 @Override onStart()212 public void onStart() { 213 super.onStart(); 214 updateStage(mUiStage); 215 216 if (mSaveLockWorker != null) { 217 setPrimaryButtonEnabled(true); 218 mSaveLockWorker.setListener(this::onChosenLockSaveFinished); 219 } 220 } 221 222 @Override onSaveInstanceState(Bundle outState)223 public void onSaveInstanceState(Bundle outState) { 224 super.onSaveInstanceState(outState); 225 outState.putInt(STATE_UI_STAGE, mUiStage.ordinal()); 226 outState.putParcelable(STATE_CHOSEN_PATTERN, mChosenPattern); 227 } 228 229 @Override onStop()230 public void onStop() { 231 super.onStop(); 232 if (mSaveLockWorker != null) { 233 mSaveLockWorker.setListener(null); 234 } 235 getToolbar().getProgressBar().setVisible(false); 236 } 237 238 @Override onDestroy()239 public void onDestroy() { 240 super.onDestroy(); 241 242 mLockPatternView.clearPattern(); 243 244 PasswordHelper.zeroizeCredentials(mChosenPattern, mCurrentCredential); 245 } 246 247 /** 248 * Updates the messages and buttons appropriate to what stage the user 249 * is at in choosing a pattern. This doesn't handle clearing out the pattern; 250 * the pattern is expected to be in the right state. 251 * 252 * @param stage The stage UI should be updated to match with. 253 */ updateStage(Stage stage)254 protected void updateStage(Stage stage) { 255 mUiStage = stage; 256 257 // Message mText, visibility and 258 // mEnabled state all known from the stage 259 mMessageText.setText(stage.mMessageId); 260 261 if (stage.mSecondaryButtonState == SecondaryButtonState.Gone) { 262 setSecondaryButtonVisible(false); 263 } else { 264 setSecondaryButtonVisible(true); 265 setSecondaryButtonText(stage.mSecondaryButtonState.mTextResId); 266 setSecondaryButtonEnabled(stage.mSecondaryButtonState.mEnabled); 267 } 268 269 setPrimaryButtonText(stage.mPrimaryButtonState.mText); 270 setPrimaryButtonEnabled(stage.mPrimaryButtonState.mEnabled); 271 272 // same for whether the pattern is mEnabled 273 if (stage.mPatternEnabled) { 274 mLockPatternView.enableInput(); 275 } else { 276 mLockPatternView.disableInput(); 277 } 278 279 // the rest of the stuff varies enough that it is easier just to handle 280 // on a case by case basis. 281 mLockPatternView.setDisplayMode(DisplayMode.Correct); 282 283 switch (mUiStage) { 284 case Introduction: 285 mLockPatternView.clearPattern(); 286 break; 287 case HelpScreen: 288 mLockPatternView.setPattern(DisplayMode.Animate, mAnimatePattern); 289 break; 290 case ChoiceTooShort: 291 mLockPatternView.setDisplayMode(DisplayMode.Wrong); 292 postClearPatternRunnable(); 293 break; 294 case FirstChoiceValid: 295 break; 296 case NeedToConfirm: 297 mLockPatternView.clearPattern(); 298 break; 299 case ConfirmWrong: 300 mLockPatternView.setDisplayMode(DisplayMode.Wrong); 301 postClearPatternRunnable(); 302 break; 303 case ChoiceConfirmed: 304 break; 305 default: 306 // Do nothing. 307 } 308 } 309 updateUIWhenPatternInProgress()310 private void updateUIWhenPatternInProgress() { 311 mMessageText.setText(R.string.lockpattern_recording_inprogress); 312 setPrimaryButtonEnabled(false); 313 setSecondaryButtonEnabled(false); 314 } 315 316 // clear the wrong pattern unless they have started a new one 317 // already postClearPatternRunnable()318 private void postClearPatternRunnable() { 319 mLockPatternView.removeCallbacks(mClearPatternRunnable); 320 mLockPatternView.postDelayed(mClearPatternRunnable, mWrongPatternClearTimeOut); 321 } 322 setPrimaryButtonEnabled(boolean enabled)323 private void setPrimaryButtonEnabled(boolean enabled) { 324 mPrimaryButton.setEnabled(enabled); 325 } 326 setPrimaryButtonText(@tringRes int textId)327 private void setPrimaryButtonText(@StringRes int textId) { 328 mPrimaryButton.setTitle(textId); 329 } 330 setSecondaryButtonVisible(boolean visible)331 private void setSecondaryButtonVisible(boolean visible) { 332 mSecondaryButton.setVisible(visible); 333 } 334 setSecondaryButtonEnabled(boolean enabled)335 private void setSecondaryButtonEnabled(boolean enabled) { 336 mSecondaryButton.setEnabled(enabled); 337 } 338 setSecondaryButtonText(@tringRes int textId)339 private void setSecondaryButtonText(@StringRes int textId) { 340 mSecondaryButton.setTitle(textId); 341 } 342 343 // Update display message and decide on next step according to the different mText 344 // on the primary button handlePrimaryButtonClick()345 private void handlePrimaryButtonClick() { 346 switch (mUiStage.mPrimaryButtonState) { 347 case Continue: 348 if (mUiStage != Stage.FirstChoiceValid) { 349 throw new IllegalStateException("expected ui stage " 350 + Stage.FirstChoiceValid + " when button is " 351 + PrimaryButtonState.Continue); 352 } 353 updateStage(Stage.NeedToConfirm); 354 break; 355 case Confirm: 356 if (mUiStage != Stage.ChoiceConfirmed) { 357 throw new IllegalStateException("expected ui stage " + Stage.ChoiceConfirmed 358 + " when button is " + PrimaryButtonState.Confirm); 359 } 360 startSaveAndFinish(); 361 break; 362 case Retry: 363 if (mUiStage != Stage.SaveFailure) { 364 throw new IllegalStateException("expected ui stage " + Stage.SaveFailure 365 + " when button is " + PrimaryButtonState.Retry); 366 } 367 startSaveAndFinish(); 368 break; 369 case Ok: 370 if (mUiStage != Stage.HelpScreen) { 371 throw new IllegalStateException("Help screen is only mode with ok button, " 372 + "but stage is " + mUiStage); 373 } 374 mLockPatternView.clearPattern(); 375 mLockPatternView.setDisplayMode(DisplayMode.Correct); 376 updateStage(Stage.Introduction); 377 break; 378 default: 379 // Do nothing. 380 } 381 } 382 383 // Update display message and proceed to next step according to the different mText on 384 // the secondary button. handleSecondaryButtonClick()385 private void handleSecondaryButtonClick() { 386 switch (mUiStage.mSecondaryButtonState) { 387 case Retry: 388 mChosenPattern = null; 389 mLockPatternView.clearPattern(); 390 updateStage(Stage.Introduction); 391 break; 392 case Cancel: 393 getFragmentHost().goBack(); 394 break; 395 default: 396 throw new IllegalStateException("secondary footer button pressed, but stage of " 397 + mUiStage + " doesn't make sense"); 398 } 399 } 400 401 @VisibleForTesting onChosenLockSaveFinished(boolean isSaveSuccessful)402 void onChosenLockSaveFinished(boolean isSaveSuccessful) { 403 getToolbar().getProgressBar().setVisible(false); 404 405 if (isSaveSuccessful) { 406 onComplete(); 407 } else { 408 updateStage(Stage.SaveFailure); 409 } 410 } 411 412 // Save recorded pattern as an async task and proceed to next startSaveAndFinish()413 private void startSaveAndFinish() { 414 if (mSaveLockWorker != null && !mSaveLockWorker.isFinished()) { 415 LOG.v("startSaveAndFinish with a running SavePatternWorker."); 416 return; 417 } 418 419 setPrimaryButtonEnabled(false); 420 421 if (mSaveLockWorker == null) { 422 mSaveLockWorker = new SaveLockWorker(); 423 mSaveLockWorker.setListener(this::onChosenLockSaveFinished); 424 425 getFragmentManager() 426 .beginTransaction() 427 .add(mSaveLockWorker, FRAGMENT_TAG_SAVE_PATTERN_WORKER) 428 .commitNow(); 429 } 430 431 mSaveLockWorker.start(mUserId, mChosenPattern, mCurrentCredential); 432 getToolbar().getProgressBar().setVisible(true); 433 } 434 435 @VisibleForTesting onComplete()436 void onComplete() { 437 if (mCurrentCredential != null) { 438 mCurrentCredential.zeroize(); 439 } 440 441 getActivity().finish(); 442 } 443 444 /** 445 * Keep track internally of where the user is in choosing a pattern. 446 */ 447 enum Stage { 448 /** 449 * Initial stage when first launching choose a lock pattern. 450 * Pattern mEnabled, secondary button allow for Cancel, primary button disabled. 451 */ 452 Introduction( 453 R.string.lockpattern_recording_intro_header, 454 SecondaryButtonState.Cancel, 455 PrimaryButtonState.ContinueDisabled, 456 /* patternEnabled= */ true), 457 /** 458 * Help screen to show how a valid pattern looks like. 459 * Pattern disabled, primary button shows Ok. No secondary button. 460 */ 461 HelpScreen( 462 R.string.lockpattern_settings_help_how_to_record, 463 SecondaryButtonState.Gone, 464 PrimaryButtonState.Ok, 465 /* patternEnabled= */ false), 466 /** 467 * Invalid pattern is entered, hint message show required number of dots. 468 * Secondary button allows for Retry, primary button disabled. 469 */ 470 ChoiceTooShort( 471 R.string.lockpattern_recording_incorrect_too_short, 472 SecondaryButtonState.Retry, 473 PrimaryButtonState.ContinueDisabled, 474 /* patternEnabled= */ true), 475 /** 476 * First drawing on the pattern is valid, primary button shows Continue, 477 * can proceed to next screen. 478 */ 479 FirstChoiceValid( 480 R.string.lockpattern_recording_intro_header, 481 SecondaryButtonState.Retry, 482 PrimaryButtonState.Continue, 483 /* patternEnabled= */ false), 484 /** 485 * Need to draw pattern again to confirm. 486 * Secondary button allows for Cancel, primary button disabled. 487 */ 488 NeedToConfirm( 489 R.string.lockpattern_need_to_confirm, 490 SecondaryButtonState.Cancel, 491 PrimaryButtonState.ConfirmDisabled, 492 /* patternEnabled= */ true), 493 /** 494 * Confirmation of previous drawn pattern failed, didn't enter the same pattern. 495 * Need to re-draw the pattern to match the fist pattern. 496 */ 497 ConfirmWrong( 498 R.string.lockpattern_pattern_wrong, 499 SecondaryButtonState.Cancel, 500 PrimaryButtonState.ConfirmDisabled, 501 /* patternEnabled= */ true), 502 /** 503 * Pattern is confirmed after drawing the same pattern twice. 504 * Pattern disabled. 505 */ 506 ChoiceConfirmed( 507 R.string.lockpattern_pattern_confirmed, 508 SecondaryButtonState.Cancel, 509 PrimaryButtonState.Confirm, 510 /* patternEnabled= */ false), 511 512 /** 513 * Error saving pattern. 514 * Pattern disabled, primary button shows Retry, secondary button allows for cancel 515 */ 516 SaveFailure( 517 R.string.error_saving_lockpattern, 518 SecondaryButtonState.Cancel, 519 PrimaryButtonState.Retry, 520 /* patternEnabled= */ false); 521 522 final int mMessageId; 523 final SecondaryButtonState mSecondaryButtonState; 524 final PrimaryButtonState mPrimaryButtonState; 525 final boolean mPatternEnabled; 526 527 /** 528 * @param messageId The message displayed as instruction. 529 * @param secondaryButtonState The state of the secondary button. 530 * @param primaryButtonState The state of the primary button. 531 * @param patternEnabled Whether the pattern widget is mEnabled. 532 */ Stage(@tringRes int messageId, SecondaryButtonState secondaryButtonState, PrimaryButtonState primaryButtonState, boolean patternEnabled)533 Stage(@StringRes int messageId, 534 SecondaryButtonState secondaryButtonState, 535 PrimaryButtonState primaryButtonState, 536 boolean patternEnabled) { 537 this.mMessageId = messageId; 538 this.mSecondaryButtonState = secondaryButtonState; 539 this.mPrimaryButtonState = primaryButtonState; 540 this.mPatternEnabled = patternEnabled; 541 } 542 } 543 544 /** 545 * The states of the primary footer button. 546 */ 547 enum PrimaryButtonState { 548 Continue(R.string.continue_button_text, true), 549 ContinueDisabled(R.string.continue_button_text, false), 550 Confirm(R.string.lockpattern_confirm_button_text, true), 551 ConfirmDisabled(R.string.lockpattern_confirm_button_text, false), 552 Retry(R.string.lockscreen_retry_button_text, true), 553 Ok(R.string.okay, true); 554 555 final int mText; 556 final boolean mEnabled; 557 558 /** 559 * @param text The displayed mText for this mode. 560 * @param enabled Whether the button should be mEnabled. 561 */ PrimaryButtonState(@tringRes int text, boolean enabled)562 PrimaryButtonState(@StringRes int text, boolean enabled) { 563 this.mText = text; 564 this.mEnabled = enabled; 565 } 566 } 567 568 /** 569 * The states of the secondary footer button. 570 */ 571 enum SecondaryButtonState { 572 Cancel(R.string.lockpattern_cancel_button_text, true), 573 CancelDisabled(R.string.lockpattern_cancel_button_text, false), 574 Retry(R.string.lockpattern_retry_button_text, true), 575 RetryDisabled(R.string.lockpattern_retry_button_text, false), 576 Gone(ID_EMPTY_MESSAGE, false); 577 578 final int mTextResId; 579 final boolean mEnabled; 580 581 /** 582 * @param textId The displayed mText for this mode. 583 * @param enabled Whether the button should be mEnabled. 584 */ SecondaryButtonState(@tringRes int textId, boolean enabled)585 SecondaryButtonState(@StringRes int textId, boolean enabled) { 586 this.mTextResId = textId; 587 this.mEnabled = enabled; 588 } 589 } 590 } 591