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