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