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