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 com.android.settings.ChooseLockGeneric.ChooseLockGenericFragment; 26 27 import static com.android.internal.widget.LockPatternView.DisplayMode; 28 29 import android.app.Activity; 30 import android.app.Fragment; 31 import android.content.Intent; 32 import android.os.Bundle; 33 import android.preference.PreferenceActivity; 34 import android.view.KeyEvent; 35 import android.view.LayoutInflater; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.widget.TextView; 39 40 import java.util.ArrayList; 41 import java.util.Collections; 42 import 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 */ 52 public 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 if (!(getActivity() instanceof ChooseLockPattern)) { 312 throw new SecurityException("Fragment contained in wrong activity"); 313 } 314 } 315 316 @Override 317 public View onCreateView(LayoutInflater inflater, ViewGroup container, 318 Bundle savedInstanceState) { 319 320 // setupViews() 321 View view = inflater.inflate(R.layout.choose_lock_pattern, null); 322 mHeaderText = (TextView) view.findViewById(R.id.headerText); 323 mLockPatternView = (LockPatternView) view.findViewById(R.id.lockPattern); 324 mLockPatternView.setOnPatternListener(mChooseNewLockPatternListener); 325 mLockPatternView.setTactileFeedbackEnabled( 326 mChooseLockSettingsHelper.utils().isTactileFeedbackEnabled()); 327 328 mFooterText = (TextView) view.findViewById(R.id.footerText); 329 330 mFooterLeftButton = (TextView) view.findViewById(R.id.footerLeftButton); 331 mFooterRightButton = (TextView) view.findViewById(R.id.footerRightButton); 332 333 mFooterLeftButton.setOnClickListener(this); 334 mFooterRightButton.setOnClickListener(this); 335 336 // make it so unhandled touch events within the unlock screen go to the 337 // lock pattern view. 338 final LinearLayoutWithDefaultTouchRecepient topLayout 339 = (LinearLayoutWithDefaultTouchRecepient) view.findViewById( 340 R.id.topLayout); 341 topLayout.setDefaultTouchRecepient(mLockPatternView); 342 343 final boolean confirmCredentials = getActivity().getIntent() 344 .getBooleanExtra("confirm_credentials", true); 345 346 if (savedInstanceState == null) { 347 if (confirmCredentials) { 348 // first launch. As a security measure, we're in NeedToConfirm mode until we 349 // know there isn't an existing password or the user confirms their password. 350 updateStage(Stage.NeedToConfirm); 351 boolean launchedConfirmationActivity = 352 mChooseLockSettingsHelper.launchConfirmationActivity( 353 CONFIRM_EXISTING_REQUEST, null, null); 354 if (!launchedConfirmationActivity) { 355 updateStage(Stage.Introduction); 356 } 357 } else { 358 updateStage(Stage.Introduction); 359 } 360 } else { 361 // restore from previous state 362 final String patternString = savedInstanceState.getString(KEY_PATTERN_CHOICE); 363 if (patternString != null) { 364 mChosenPattern = LockPatternUtils.stringToPattern(patternString); 365 } 366 updateStage(Stage.values()[savedInstanceState.getInt(KEY_UI_STAGE)]); 367 } 368 return view; 369 } 370 371 public void onClick(View v) { 372 if (v == mFooterLeftButton) { 373 if (mUiStage.leftMode == LeftButtonMode.Retry) { 374 mChosenPattern = null; 375 mLockPatternView.clearPattern(); 376 updateStage(Stage.Introduction); 377 } else if (mUiStage.leftMode == LeftButtonMode.Cancel) { 378 // They are canceling the entire wizard 379 getActivity().setResult(RESULT_FINISHED); 380 getActivity().finish(); 381 } else { 382 throw new IllegalStateException("left footer button pressed, but stage of " + 383 mUiStage + " doesn't make sense"); 384 } 385 } else if (v == mFooterRightButton) { 386 387 if (mUiStage.rightMode == RightButtonMode.Continue) { 388 if (mUiStage != Stage.FirstChoiceValid) { 389 throw new IllegalStateException("expected ui stage " + Stage.FirstChoiceValid 390 + " when button is " + RightButtonMode.Continue); 391 } 392 updateStage(Stage.NeedToConfirm); 393 } else if (mUiStage.rightMode == RightButtonMode.Confirm) { 394 if (mUiStage != Stage.ChoiceConfirmed) { 395 throw new IllegalStateException("expected ui stage " + Stage.ChoiceConfirmed 396 + " when button is " + RightButtonMode.Confirm); 397 } 398 saveChosenPatternAndFinish(); 399 } else if (mUiStage.rightMode == RightButtonMode.Ok) { 400 if (mUiStage != Stage.HelpScreen) { 401 throw new IllegalStateException("Help screen is only mode with ok button, but " + 402 "stage is " + mUiStage); 403 } 404 mLockPatternView.clearPattern(); 405 mLockPatternView.setDisplayMode(DisplayMode.Correct); 406 updateStage(Stage.Introduction); 407 } 408 } 409 } 410 411 public boolean onKeyDown(int keyCode, KeyEvent event) { 412 if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { 413 if (mUiStage == Stage.HelpScreen) { 414 updateStage(Stage.Introduction); 415 return true; 416 } 417 } 418 if (keyCode == KeyEvent.KEYCODE_MENU && mUiStage == Stage.Introduction) { 419 updateStage(Stage.HelpScreen); 420 return true; 421 } 422 return false; 423 } 424 425 public void onSaveInstanceState(Bundle outState) { 426 super.onSaveInstanceState(outState); 427 428 outState.putInt(KEY_UI_STAGE, mUiStage.ordinal()); 429 if (mChosenPattern != null) { 430 outState.putString(KEY_PATTERN_CHOICE, 431 LockPatternUtils.patternToString(mChosenPattern)); 432 } 433 } 434 435 /** 436 * Updates the messages and buttons appropriate to what stage the user 437 * is at in choosing a view. This doesn't handle clearing out the pattern; 438 * the pattern is expected to be in the right state. 439 * @param stage 440 */ 441 protected void updateStage(Stage stage) { 442 final Stage previousStage = mUiStage; 443 444 mUiStage = stage; 445 446 // header text, footer text, visibility and 447 // enabled state all known from the stage 448 if (stage == Stage.ChoiceTooShort) { 449 mHeaderText.setText( 450 getResources().getString( 451 stage.headerMessage, 452 LockPatternUtils.MIN_LOCK_PATTERN_SIZE)); 453 } else { 454 mHeaderText.setText(stage.headerMessage); 455 } 456 if (stage.footerMessage == ID_EMPTY_MESSAGE) { 457 mFooterText.setText(""); 458 } else { 459 mFooterText.setText(stage.footerMessage); 460 } 461 462 if (stage.leftMode == LeftButtonMode.Gone) { 463 mFooterLeftButton.setVisibility(View.GONE); 464 } else { 465 mFooterLeftButton.setVisibility(View.VISIBLE); 466 mFooterLeftButton.setText(stage.leftMode.text); 467 mFooterLeftButton.setEnabled(stage.leftMode.enabled); 468 } 469 470 mFooterRightButton.setText(stage.rightMode.text); 471 mFooterRightButton.setEnabled(stage.rightMode.enabled); 472 473 // same for whether the patten is enabled 474 if (stage.patternEnabled) { 475 mLockPatternView.enableInput(); 476 } else { 477 mLockPatternView.disableInput(); 478 } 479 480 // the rest of the stuff varies enough that it is easier just to handle 481 // on a case by case basis. 482 mLockPatternView.setDisplayMode(DisplayMode.Correct); 483 484 switch (mUiStage) { 485 case Introduction: 486 mLockPatternView.clearPattern(); 487 break; 488 case HelpScreen: 489 mLockPatternView.setPattern(DisplayMode.Animate, mAnimatePattern); 490 break; 491 case ChoiceTooShort: 492 mLockPatternView.setDisplayMode(DisplayMode.Wrong); 493 postClearPatternRunnable(); 494 break; 495 case FirstChoiceValid: 496 break; 497 case NeedToConfirm: 498 mLockPatternView.clearPattern(); 499 break; 500 case ConfirmWrong: 501 mLockPatternView.setDisplayMode(DisplayMode.Wrong); 502 postClearPatternRunnable(); 503 break; 504 case ChoiceConfirmed: 505 break; 506 } 507 508 // If the stage changed, announce the header for accessibility. This 509 // is a no-op when accessibility is disabled. 510 if (previousStage != stage) { 511 mHeaderText.announceForAccessibility(mHeaderText.getText()); 512 } 513 } 514 515 516 // clear the wrong pattern unless they have started a new one 517 // already 518 private void postClearPatternRunnable() { 519 mLockPatternView.removeCallbacks(mClearPatternRunnable); 520 mLockPatternView.postDelayed(mClearPatternRunnable, WRONG_PATTERN_CLEAR_TIMEOUT_MS); 521 } 522 523 private void saveChosenPatternAndFinish() { 524 LockPatternUtils utils = mChooseLockSettingsHelper.utils(); 525 final boolean lockVirgin = !utils.isPatternEverChosen(); 526 527 final boolean isFallback = getActivity().getIntent() 528 .getBooleanExtra(LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK, false); 529 utils.saveLockPattern(mChosenPattern, isFallback); 530 utils.setLockPatternEnabled(true); 531 532 if (lockVirgin) { 533 utils.setVisiblePatternEnabled(true); 534 } 535 536 getActivity().setResult(RESULT_FINISHED); 537 getActivity().finish(); 538 } 539 } 540 } 541