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