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