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