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