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