ChooseLockPattern.java revision 6609b0c22ae89a24d1b07dc3c4143452616d4450
1/* 2 * Copyright (C) 2007 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 17package com.android.settings; 18 19import com.android.internal.logging.MetricsLogger; 20import com.google.android.collect.Lists; 21import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient; 22import com.android.internal.widget.LockPatternUtils; 23import com.android.internal.widget.LockPatternView; 24import com.android.internal.widget.LockPatternView.Cell; 25import com.android.settings.notification.RedactionInterstitial; 26 27import static com.android.internal.widget.LockPatternView.DisplayMode; 28 29import android.app.Activity; 30import android.app.Fragment; 31import android.content.Context; 32import android.content.Intent; 33import android.os.Bundle; 34import android.view.KeyEvent; 35import android.view.LayoutInflater; 36import android.view.View; 37import android.view.ViewGroup; 38import android.widget.TextView; 39 40import java.util.ArrayList; 41import java.util.Collections; 42import java.util.List; 43 44/** 45 * If the user has a lock pattern set already, makes them confirm the existing one. 46 * 47 * Then, prompts the user to choose a lock pattern: 48 * - prompts for initial pattern 49 * - asks for confirmation / restart 50 * - saves chosen password when confirmed 51 */ 52public class ChooseLockPattern extends SettingsActivity { 53 /** 54 * Used by the choose lock pattern wizard to indicate the wizard is 55 * finished, and each activity in the wizard should finish. 56 * <p> 57 * Previously, each activity in the wizard would finish itself after 58 * starting the next activity. However, this leads to broken 'Back' 59 * behavior. So, now an activity does not finish itself until it gets this 60 * result. 61 */ 62 static final int RESULT_FINISHED = RESULT_FIRST_USER; 63 64 @Override 65 public Intent getIntent() { 66 Intent modIntent = new Intent(super.getIntent()); 67 modIntent.putExtra(EXTRA_SHOW_FRAGMENT, getFragmentClass().getName()); 68 return modIntent; 69 } 70 71 public static Intent createIntent(Context context, 72 boolean requirePassword, boolean confirmCredentials) { 73 Intent intent = new Intent(context, ChooseLockPattern.class); 74 intent.putExtra("key_lock_method", "pattern"); 75 intent.putExtra(ChooseLockGeneric.CONFIRM_CREDENTIALS, confirmCredentials); 76 intent.putExtra(EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, requirePassword); 77 return intent; 78 } 79 80 public static Intent createIntent(Context context, 81 boolean requirePassword, String pattern) { 82 Intent intent = createIntent(context, requirePassword, false); 83 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, pattern); 84 return intent; 85 } 86 87 88 public static Intent createIntent(Context context, 89 boolean requirePassword, long challenge) { 90 Intent intent = createIntent(context, requirePassword, false); 91 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, true); 92 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, challenge); 93 return intent; 94 } 95 96 @Override 97 protected boolean isValidFragment(String fragmentName) { 98 if (ChooseLockPatternFragment.class.getName().equals(fragmentName)) return true; 99 return false; 100 } 101 102 /* package */ Class<? extends Fragment> getFragmentClass() { 103 return ChooseLockPatternFragment.class; 104 } 105 106 @Override 107 public void onCreate(Bundle savedInstanceState) { 108 // requestWindowFeature(Window.FEATURE_NO_TITLE); 109 super.onCreate(savedInstanceState); 110 CharSequence msg = getText(R.string.lockpassword_choose_your_pattern_header); 111 setTitle(msg); 112 } 113 114 @Override 115 public boolean onKeyDown(int keyCode, KeyEvent event) { 116 // *** TODO *** 117 // chooseLockPatternFragment.onKeyDown(keyCode, event); 118 return super.onKeyDown(keyCode, event); 119 } 120 121 public static class ChooseLockPatternFragment extends InstrumentedFragment 122 implements View.OnClickListener { 123 124 public static final int CONFIRM_EXISTING_REQUEST = 55; 125 126 // how long after a confirmation message is shown before moving on 127 static final int INFORMATION_MSG_TIMEOUT_MS = 3000; 128 129 // how long we wait to clear a wrong pattern 130 private static final int WRONG_PATTERN_CLEAR_TIMEOUT_MS = 2000; 131 132 private static final int ID_EMPTY_MESSAGE = -1; 133 134 private String mCurrentPattern; 135 private boolean mHasChallenge; 136 private long mChallenge; 137 protected TextView mHeaderText; 138 protected LockPatternView mLockPatternView; 139 protected TextView mFooterText; 140 private TextView mFooterLeftButton; 141 private TextView mFooterRightButton; 142 protected List<LockPatternView.Cell> mChosenPattern = null; 143 144 /** 145 * The patten used during the help screen to show how to draw a pattern. 146 */ 147 private final List<LockPatternView.Cell> mAnimatePattern = 148 Collections.unmodifiableList(Lists.newArrayList( 149 LockPatternView.Cell.of(0, 0), 150 LockPatternView.Cell.of(0, 1), 151 LockPatternView.Cell.of(1, 1), 152 LockPatternView.Cell.of(2, 1) 153 )); 154 155 @Override 156 public void onActivityResult(int requestCode, int resultCode, 157 Intent data) { 158 super.onActivityResult(requestCode, resultCode, data); 159 switch (requestCode) { 160 case CONFIRM_EXISTING_REQUEST: 161 if (resultCode != Activity.RESULT_OK) { 162 getActivity().setResult(RESULT_FINISHED); 163 getActivity().finish(); 164 } else { 165 mCurrentPattern = data.getStringExtra( 166 ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); 167 } 168 169 updateStage(Stage.Introduction); 170 break; 171 } 172 } 173 174 protected void setRightButtonEnabled(boolean enabled) { 175 mFooterRightButton.setEnabled(enabled); 176 } 177 178 protected void setRightButtonText(int text) { 179 mFooterRightButton.setText(text); 180 } 181 182 /** 183 * The pattern listener that responds according to a user choosing a new 184 * lock pattern. 185 */ 186 protected LockPatternView.OnPatternListener mChooseNewLockPatternListener = 187 new LockPatternView.OnPatternListener() { 188 189 public void onPatternStart() { 190 mLockPatternView.removeCallbacks(mClearPatternRunnable); 191 patternInProgress(); 192 } 193 194 public void onPatternCleared() { 195 mLockPatternView.removeCallbacks(mClearPatternRunnable); 196 } 197 198 public void onPatternDetected(List<LockPatternView.Cell> pattern) { 199 if (mUiStage == Stage.NeedToConfirm || mUiStage == Stage.ConfirmWrong) { 200 if (mChosenPattern == null) throw new IllegalStateException( 201 "null chosen pattern in stage 'need to confirm"); 202 if (mChosenPattern.equals(pattern)) { 203 updateStage(Stage.ChoiceConfirmed); 204 } else { 205 updateStage(Stage.ConfirmWrong); 206 } 207 } else if (mUiStage == Stage.Introduction || mUiStage == Stage.ChoiceTooShort){ 208 if (pattern.size() < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) { 209 updateStage(Stage.ChoiceTooShort); 210 } else { 211 mChosenPattern = new ArrayList<LockPatternView.Cell>(pattern); 212 updateStage(Stage.FirstChoiceValid); 213 } 214 } else { 215 throw new IllegalStateException("Unexpected stage " + mUiStage + " when " 216 + "entering the pattern."); 217 } 218 } 219 220 public void onPatternCellAdded(List<Cell> pattern) { 221 222 } 223 224 private void patternInProgress() { 225 mHeaderText.setText(R.string.lockpattern_recording_inprogress); 226 mFooterText.setText(""); 227 mFooterLeftButton.setEnabled(false); 228 mFooterRightButton.setEnabled(false); 229 } 230 }; 231 232 @Override 233 protected int getMetricsCategory() { 234 return MetricsLogger.CHOOSE_LOCK_PATTERN; 235 } 236 237 238 /** 239 * The states of the left footer button. 240 */ 241 enum LeftButtonMode { 242 Cancel(R.string.cancel, true), 243 CancelDisabled(R.string.cancel, false), 244 Retry(R.string.lockpattern_retry_button_text, true), 245 RetryDisabled(R.string.lockpattern_retry_button_text, false), 246 Gone(ID_EMPTY_MESSAGE, false); 247 248 249 /** 250 * @param text The displayed text for this mode. 251 * @param enabled Whether the button should be enabled. 252 */ 253 LeftButtonMode(int text, boolean enabled) { 254 this.text = text; 255 this.enabled = enabled; 256 } 257 258 final int text; 259 final boolean enabled; 260 } 261 262 /** 263 * The states of the right button. 264 */ 265 enum RightButtonMode { 266 Continue(R.string.lockpattern_continue_button_text, true), 267 ContinueDisabled(R.string.lockpattern_continue_button_text, false), 268 Confirm(R.string.lockpattern_confirm_button_text, true), 269 ConfirmDisabled(R.string.lockpattern_confirm_button_text, false), 270 Ok(android.R.string.ok, true); 271 272 /** 273 * @param text The displayed text for this mode. 274 * @param enabled Whether the button should be enabled. 275 */ 276 RightButtonMode(int text, boolean enabled) { 277 this.text = text; 278 this.enabled = enabled; 279 } 280 281 final int text; 282 final boolean enabled; 283 } 284 285 /** 286 * Keep track internally of where the user is in choosing a pattern. 287 */ 288 protected enum Stage { 289 290 Introduction( 291 R.string.lockpattern_recording_intro_header, 292 LeftButtonMode.Cancel, RightButtonMode.ContinueDisabled, 293 ID_EMPTY_MESSAGE, true), 294 HelpScreen( 295 R.string.lockpattern_settings_help_how_to_record, 296 LeftButtonMode.Gone, RightButtonMode.Ok, ID_EMPTY_MESSAGE, false), 297 ChoiceTooShort( 298 R.string.lockpattern_recording_incorrect_too_short, 299 LeftButtonMode.Retry, RightButtonMode.ContinueDisabled, 300 ID_EMPTY_MESSAGE, true), 301 FirstChoiceValid( 302 R.string.lockpattern_pattern_entered_header, 303 LeftButtonMode.Retry, RightButtonMode.Continue, ID_EMPTY_MESSAGE, false), 304 NeedToConfirm( 305 R.string.lockpattern_need_to_confirm, 306 LeftButtonMode.Cancel, RightButtonMode.ConfirmDisabled, 307 ID_EMPTY_MESSAGE, true), 308 ConfirmWrong( 309 R.string.lockpattern_need_to_unlock_wrong, 310 LeftButtonMode.Cancel, RightButtonMode.ConfirmDisabled, 311 ID_EMPTY_MESSAGE, true), 312 ChoiceConfirmed( 313 R.string.lockpattern_pattern_confirmed_header, 314 LeftButtonMode.Cancel, RightButtonMode.Confirm, ID_EMPTY_MESSAGE, false); 315 316 317 /** 318 * @param headerMessage The message displayed at the top. 319 * @param leftMode The mode of the left button. 320 * @param rightMode The mode of the right button. 321 * @param footerMessage The footer message. 322 * @param patternEnabled Whether the pattern widget is enabled. 323 */ 324 Stage(int headerMessage, 325 LeftButtonMode leftMode, 326 RightButtonMode rightMode, 327 int footerMessage, boolean patternEnabled) { 328 this.headerMessage = headerMessage; 329 this.leftMode = leftMode; 330 this.rightMode = rightMode; 331 this.footerMessage = footerMessage; 332 this.patternEnabled = patternEnabled; 333 } 334 335 final int headerMessage; 336 final LeftButtonMode leftMode; 337 final RightButtonMode rightMode; 338 final int footerMessage; 339 final boolean patternEnabled; 340 } 341 342 private Stage mUiStage = Stage.Introduction; 343 private boolean mDone = false; 344 345 private Runnable mClearPatternRunnable = new Runnable() { 346 public void run() { 347 mLockPatternView.clearPattern(); 348 } 349 }; 350 351 private ChooseLockSettingsHelper mChooseLockSettingsHelper; 352 353 private static final String KEY_UI_STAGE = "uiStage"; 354 private static final String KEY_PATTERN_CHOICE = "chosenPattern"; 355 private static final String KEY_CURRENT_PATTERN = "currentPattern"; 356 357 @Override 358 public void onCreate(Bundle savedInstanceState) { 359 super.onCreate(savedInstanceState); 360 mChooseLockSettingsHelper = new ChooseLockSettingsHelper(getActivity()); 361 if (!(getActivity() instanceof ChooseLockPattern)) { 362 throw new SecurityException("Fragment contained in wrong activity"); 363 } 364 } 365 366 @Override 367 public View onCreateView(LayoutInflater inflater, ViewGroup container, 368 Bundle savedInstanceState) { 369 return inflater.inflate(R.layout.choose_lock_pattern, container, false); 370 } 371 372 @Override 373 public void onViewCreated(View view, Bundle savedInstanceState) { 374 super.onViewCreated(view, savedInstanceState); 375 mHeaderText = (TextView) view.findViewById(R.id.headerText); 376 mLockPatternView = (LockPatternView) view.findViewById(R.id.lockPattern); 377 mLockPatternView.setOnPatternListener(mChooseNewLockPatternListener); 378 mLockPatternView.setTactileFeedbackEnabled( 379 mChooseLockSettingsHelper.utils().isTactileFeedbackEnabled()); 380 381 mFooterText = (TextView) view.findViewById(R.id.footerText); 382 383 mFooterLeftButton = (TextView) view.findViewById(R.id.footerLeftButton); 384 mFooterRightButton = (TextView) view.findViewById(R.id.footerRightButton); 385 386 mFooterLeftButton.setOnClickListener(this); 387 mFooterRightButton.setOnClickListener(this); 388 389 // make it so unhandled touch events within the unlock screen go to the 390 // lock pattern view. 391 final LinearLayoutWithDefaultTouchRecepient topLayout 392 = (LinearLayoutWithDefaultTouchRecepient) view.findViewById( 393 R.id.topLayout); 394 topLayout.setDefaultTouchRecepient(mLockPatternView); 395 396 final boolean confirmCredentials = getActivity().getIntent() 397 .getBooleanExtra("confirm_credentials", true); 398 Intent intent = getActivity().getIntent(); 399 mCurrentPattern = intent.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); 400 mHasChallenge = intent.getBooleanExtra( 401 ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, false); 402 mChallenge = intent.getLongExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, 0); 403 404 if (savedInstanceState == null) { 405 if (confirmCredentials) { 406 // first launch. As a security measure, we're in NeedToConfirm mode until we 407 // know there isn't an existing password or the user confirms their password. 408 updateStage(Stage.NeedToConfirm); 409 boolean launchedConfirmationActivity = 410 mChooseLockSettingsHelper.launchConfirmationActivity( 411 CONFIRM_EXISTING_REQUEST, 412 getString(R.string.unlock_set_unlock_launch_picker_title), true); 413 if (!launchedConfirmationActivity) { 414 updateStage(Stage.Introduction); 415 } 416 } else { 417 updateStage(Stage.Introduction); 418 } 419 } else { 420 // restore from previous state 421 final String patternString = savedInstanceState.getString(KEY_PATTERN_CHOICE); 422 if (patternString != null) { 423 mChosenPattern = LockPatternUtils.stringToPattern(patternString); 424 } 425 426 if (mCurrentPattern == null) { 427 mCurrentPattern = savedInstanceState.getString(KEY_CURRENT_PATTERN); 428 } 429 updateStage(Stage.values()[savedInstanceState.getInt(KEY_UI_STAGE)]); 430 } 431 mDone = false; 432 } 433 434 protected Intent getRedactionInterstitialIntent(Context context) { 435 return RedactionInterstitial.createStartIntent(context); 436 } 437 438 public void handleLeftButton() { 439 if (mUiStage.leftMode == LeftButtonMode.Retry) { 440 mChosenPattern = null; 441 mLockPatternView.clearPattern(); 442 updateStage(Stage.Introduction); 443 } else if (mUiStage.leftMode == LeftButtonMode.Cancel) { 444 // They are canceling the entire wizard 445 getActivity().setResult(RESULT_FINISHED); 446 getActivity().finish(); 447 } else { 448 throw new IllegalStateException("left footer button pressed, but stage of " + 449 mUiStage + " doesn't make sense"); 450 } 451 } 452 453 public void handleRightButton() { 454 if (mUiStage.rightMode == RightButtonMode.Continue) { 455 if (mUiStage != Stage.FirstChoiceValid) { 456 throw new IllegalStateException("expected ui stage " 457 + Stage.FirstChoiceValid + " when button is " 458 + RightButtonMode.Continue); 459 } 460 updateStage(Stage.NeedToConfirm); 461 } else if (mUiStage.rightMode == RightButtonMode.Confirm) { 462 if (mUiStage != Stage.ChoiceConfirmed) { 463 throw new IllegalStateException("expected ui stage " + Stage.ChoiceConfirmed 464 + " when button is " + RightButtonMode.Confirm); 465 } 466 saveChosenPatternAndFinish(); 467 } else if (mUiStage.rightMode == RightButtonMode.Ok) { 468 if (mUiStage != Stage.HelpScreen) { 469 throw new IllegalStateException("Help screen is only mode with ok button, " 470 + "but stage is " + mUiStage); 471 } 472 mLockPatternView.clearPattern(); 473 mLockPatternView.setDisplayMode(DisplayMode.Correct); 474 updateStage(Stage.Introduction); 475 } 476 } 477 478 public void onClick(View v) { 479 if (v == mFooterLeftButton) { 480 handleLeftButton(); 481 } else if (v == mFooterRightButton) { 482 handleRightButton(); 483 } 484 } 485 486 public boolean onKeyDown(int keyCode, KeyEvent event) { 487 if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { 488 if (mUiStage == Stage.HelpScreen) { 489 updateStage(Stage.Introduction); 490 return true; 491 } 492 } 493 if (keyCode == KeyEvent.KEYCODE_MENU && mUiStage == Stage.Introduction) { 494 updateStage(Stage.HelpScreen); 495 return true; 496 } 497 return false; 498 } 499 500 public void onSaveInstanceState(Bundle outState) { 501 super.onSaveInstanceState(outState); 502 503 outState.putInt(KEY_UI_STAGE, mUiStage.ordinal()); 504 if (mChosenPattern != null) { 505 outState.putString(KEY_PATTERN_CHOICE, 506 LockPatternUtils.patternToString(mChosenPattern)); 507 } 508 509 if (mCurrentPattern != null) { 510 outState.putString(KEY_CURRENT_PATTERN, 511 mCurrentPattern); 512 } 513 } 514 515 /** 516 * Updates the messages and buttons appropriate to what stage the user 517 * is at in choosing a view. This doesn't handle clearing out the pattern; 518 * the pattern is expected to be in the right state. 519 * @param stage 520 */ 521 protected void updateStage(Stage stage) { 522 final Stage previousStage = mUiStage; 523 524 mUiStage = stage; 525 526 // header text, footer text, visibility and 527 // enabled state all known from the stage 528 if (stage == Stage.ChoiceTooShort) { 529 mHeaderText.setText( 530 getResources().getString( 531 stage.headerMessage, 532 LockPatternUtils.MIN_LOCK_PATTERN_SIZE)); 533 } else { 534 mHeaderText.setText(stage.headerMessage); 535 } 536 if (stage.footerMessage == ID_EMPTY_MESSAGE) { 537 mFooterText.setText(""); 538 } else { 539 mFooterText.setText(stage.footerMessage); 540 } 541 542 if (stage.leftMode == LeftButtonMode.Gone) { 543 mFooterLeftButton.setVisibility(View.GONE); 544 } else { 545 mFooterLeftButton.setVisibility(View.VISIBLE); 546 mFooterLeftButton.setText(stage.leftMode.text); 547 mFooterLeftButton.setEnabled(stage.leftMode.enabled); 548 } 549 550 setRightButtonText(stage.rightMode.text); 551 setRightButtonEnabled(stage.rightMode.enabled); 552 553 // same for whether the patten is enabled 554 if (stage.patternEnabled) { 555 mLockPatternView.enableInput(); 556 } else { 557 mLockPatternView.disableInput(); 558 } 559 560 // the rest of the stuff varies enough that it is easier just to handle 561 // on a case by case basis. 562 mLockPatternView.setDisplayMode(DisplayMode.Correct); 563 564 switch (mUiStage) { 565 case Introduction: 566 mLockPatternView.clearPattern(); 567 break; 568 case HelpScreen: 569 mLockPatternView.setPattern(DisplayMode.Animate, mAnimatePattern); 570 break; 571 case ChoiceTooShort: 572 mLockPatternView.setDisplayMode(DisplayMode.Wrong); 573 postClearPatternRunnable(); 574 break; 575 case FirstChoiceValid: 576 break; 577 case NeedToConfirm: 578 mLockPatternView.clearPattern(); 579 break; 580 case ConfirmWrong: 581 mLockPatternView.setDisplayMode(DisplayMode.Wrong); 582 postClearPatternRunnable(); 583 break; 584 case ChoiceConfirmed: 585 break; 586 } 587 588 // If the stage changed, announce the header for accessibility. This 589 // is a no-op when accessibility is disabled. 590 if (previousStage != stage) { 591 mHeaderText.announceForAccessibility(mHeaderText.getText()); 592 } 593 } 594 595 596 // clear the wrong pattern unless they have started a new one 597 // already 598 private void postClearPatternRunnable() { 599 mLockPatternView.removeCallbacks(mClearPatternRunnable); 600 mLockPatternView.postDelayed(mClearPatternRunnable, WRONG_PATTERN_CLEAR_TIMEOUT_MS); 601 } 602 603 private void saveChosenPatternAndFinish() { 604 if (mDone) return; 605 LockPatternUtils utils = mChooseLockSettingsHelper.utils(); 606 final boolean lockVirgin = !utils.isPatternEverChosen(); 607 608 boolean wasSecureBefore = utils.isSecure(); 609 610 final boolean required = getActivity().getIntent().getBooleanExtra( 611 EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true); 612 613 utils.setCredentialRequiredToDecrypt(required); 614 utils.saveLockPattern(mChosenPattern, mCurrentPattern); 615 616 if (lockVirgin) { 617 utils.setVisiblePatternEnabled(true); 618 } 619 620 if (!wasSecureBefore) { 621 startActivity(getRedactionInterstitialIntent(getActivity())); 622 } 623 624 if (mHasChallenge) { 625 Intent intent = new Intent(); 626 byte[] token = utils.verifyPattern(mChosenPattern, mChallenge); 627 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); 628 getActivity().setResult(RESULT_FINISHED, intent); 629 } else { 630 getActivity().setResult(RESULT_FINISHED); 631 } 632 633 getActivity().finish(); 634 mDone = true; 635 } 636 } 637} 638