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