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