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