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