2 * Copyright (C) 2007 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.settings;
19 import com.google.android.collect.Lists;
20 import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient;
21 import com.android.internal.widget.LockPatternUtils;
22 import com.android.internal.widget.LockPatternView;
23 import com.android.internal.widget.LockPatternView.Cell;
24 import com.android.settings.notification.RedactionInterstitial;
26 import static com.android.internal.widget.LockPatternView.DisplayMode;
28 import android.app.Activity;
29 import android.app.Fragment;
30 import android.content.ContentResolver;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.os.Bundle;
34 import android.provider.Settings;
35 import android.view.KeyEvent;
36 import android.view.LayoutInflater;
37 import android.view.View;
38 import android.view.ViewGroup;
39 import android.widget.TextView;
41 import java.util.ArrayList;
42 import java.util.Collections;
43 import java.util.List;
46 * If the user has a lock pattern set already, makes them confirm the existing one.
48 * Then, prompts the user to choose a lock pattern:
49 * - prompts for initial pattern
50 * - asks for confirmation / restart
51 * - saves chosen password when confirmed
53 public class ChooseLockPattern extends SettingsActivity {
55 * Used by the choose lock pattern wizard to indicate the wizard is
56 * finished, and each activity in the wizard should finish.
58 * Previously, each activity in the wizard would finish itself after
59 * starting the next activity. However, this leads to broken 'Back'
60 * behavior. So, now an activity does not finish itself until it gets this
63 static final int RESULT_FINISHED = RESULT_FIRST_USER;
66 public Intent getIntent() {
67 Intent modIntent = new Intent(super.getIntent());
68 modIntent.putExtra(EXTRA_SHOW_FRAGMENT, getFragmentClass().getName());
72 public static Intent createIntent(Context context,
73 boolean requirePassword, boolean confirmCredentials) {
74 Intent intent = new Intent(context, ChooseLockPattern.class);
75 intent.putExtra("key_lock_method", "pattern");
76 intent.putExtra(ChooseLockGeneric.CONFIRM_CREDENTIALS, confirmCredentials);
77 intent.putExtra(EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, requirePassword);
82 protected boolean isValidFragment(String fragmentName) {
83 if (ChooseLockPatternFragment.class.getName().equals(fragmentName)) return true;
87 /* package */ Class<? extends Fragment> getFragmentClass() {
88 return ChooseLockPatternFragment.class;
92 public void onCreate(Bundle savedInstanceState) {
93 // requestWindowFeature(Window.FEATURE_NO_TITLE);
94 super.onCreate(savedInstanceState);
95 CharSequence msg = getText(R.string.lockpassword_choose_your_pattern_header);
100 public boolean onKeyDown(int keyCode, KeyEvent event) {
102 // chooseLockPatternFragment.onKeyDown(keyCode, event);
103 return super.onKeyDown(keyCode, event);
106 public static class ChooseLockPatternFragment extends Fragment
107 implements View.OnClickListener {
109 public static final int CONFIRM_EXISTING_REQUEST = 55;
111 // how long after a confirmation message is shown before moving on
112 static final int INFORMATION_MSG_TIMEOUT_MS = 3000;
114 // how long we wait to clear a wrong pattern
115 private static final int WRONG_PATTERN_CLEAR_TIMEOUT_MS = 2000;
117 private static final int ID_EMPTY_MESSAGE = -1;
119 protected TextView mHeaderText;
120 protected LockPatternView mLockPatternView;
121 protected TextView mFooterText;
122 private TextView mFooterLeftButton;
123 private TextView mFooterRightButton;
124 protected List<LockPatternView.Cell> mChosenPattern = null;
127 * The patten used during the help screen to show how to draw a pattern.
129 private final List<LockPatternView.Cell> mAnimatePattern =
130 Collections.unmodifiableList(Lists.newArrayList(
131 LockPatternView.Cell.of(0, 0),
132 LockPatternView.Cell.of(0, 1),
133 LockPatternView.Cell.of(1, 1),
134 LockPatternView.Cell.of(2, 1)
138 public void onActivityResult(int requestCode, int resultCode,
140 super.onActivityResult(requestCode, resultCode, data);
141 switch (requestCode) {
142 case CONFIRM_EXISTING_REQUEST:
143 if (resultCode != Activity.RESULT_OK) {
144 getActivity().setResult(RESULT_FINISHED);
145 getActivity().finish();
147 updateStage(Stage.Introduction);
152 protected void setRightButtonEnabled(boolean enabled) {
153 mFooterRightButton.setEnabled(enabled);
156 protected void setRightButtonText(int text) {
157 mFooterRightButton.setText(text);
161 * The pattern listener that responds according to a user choosing a new
164 protected LockPatternView.OnPatternListener mChooseNewLockPatternListener =
165 new LockPatternView.OnPatternListener() {
167 public void onPatternStart() {
168 mLockPatternView.removeCallbacks(mClearPatternRunnable);
172 public void onPatternCleared() {
173 mLockPatternView.removeCallbacks(mClearPatternRunnable);
176 public void onPatternDetected(List<LockPatternView.Cell> pattern) {
177 if (mUiStage == Stage.NeedToConfirm || mUiStage == Stage.ConfirmWrong) {
178 if (mChosenPattern == null) throw new IllegalStateException(
179 "null chosen pattern in stage 'need to confirm");
180 if (mChosenPattern.equals(pattern)) {
181 updateStage(Stage.ChoiceConfirmed);
183 updateStage(Stage.ConfirmWrong);
185 } else if (mUiStage == Stage.Introduction || mUiStage == Stage.ChoiceTooShort){
186 if (pattern.size() < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) {
187 updateStage(Stage.ChoiceTooShort);
189 mChosenPattern = new ArrayList<LockPatternView.Cell>(pattern);
190 updateStage(Stage.FirstChoiceValid);
193 throw new IllegalStateException("Unexpected stage " + mUiStage + " when "
194 + "entering the pattern.");
198 public void onPatternCellAdded(List<Cell> pattern) {
202 private void patternInProgress() {
203 mHeaderText.setText(R.string.lockpattern_recording_inprogress);
204 mFooterText.setText("");
205 mFooterLeftButton.setEnabled(false);
206 mFooterRightButton.setEnabled(false);
212 * The states of the left footer button.
214 enum LeftButtonMode {
215 Cancel(R.string.cancel, true),
216 CancelDisabled(R.string.cancel, false),
217 Retry(R.string.lockpattern_retry_button_text, true),
218 RetryDisabled(R.string.lockpattern_retry_button_text, false),
219 Gone(ID_EMPTY_MESSAGE, false);
223 * @param text The displayed text for this mode.
224 * @param enabled Whether the button should be enabled.
226 LeftButtonMode(int text, boolean enabled) {
228 this.enabled = enabled;
232 final boolean enabled;
236 * The states of the right button.
238 enum RightButtonMode {
239 Continue(R.string.lockpattern_continue_button_text, true),
240 ContinueDisabled(R.string.lockpattern_continue_button_text, false),
241 Confirm(R.string.lockpattern_confirm_button_text, true),
242 ConfirmDisabled(R.string.lockpattern_confirm_button_text, false),
243 Ok(android.R.string.ok, true);
246 * @param text The displayed text for this mode.
247 * @param enabled Whether the button should be enabled.
249 RightButtonMode(int text, boolean enabled) {
251 this.enabled = enabled;
255 final boolean enabled;
259 * Keep track internally of where the user is in choosing a pattern.
261 protected enum Stage {
264 R.string.lockpattern_recording_intro_header,
265 LeftButtonMode.Cancel, RightButtonMode.ContinueDisabled,
266 ID_EMPTY_MESSAGE, true),
268 R.string.lockpattern_settings_help_how_to_record,
269 LeftButtonMode.Gone, RightButtonMode.Ok, ID_EMPTY_MESSAGE, false),
271 R.string.lockpattern_recording_incorrect_too_short,
272 LeftButtonMode.Retry, RightButtonMode.ContinueDisabled,
273 ID_EMPTY_MESSAGE, true),
275 R.string.lockpattern_pattern_entered_header,
276 LeftButtonMode.Retry, RightButtonMode.Continue, ID_EMPTY_MESSAGE, false),
278 R.string.lockpattern_need_to_confirm,
279 LeftButtonMode.Cancel, RightButtonMode.ConfirmDisabled,
280 ID_EMPTY_MESSAGE, true),
282 R.string.lockpattern_need_to_unlock_wrong,
283 LeftButtonMode.Cancel, RightButtonMode.ConfirmDisabled,
284 ID_EMPTY_MESSAGE, true),
286 R.string.lockpattern_pattern_confirmed_header,
287 LeftButtonMode.Cancel, RightButtonMode.Confirm, ID_EMPTY_MESSAGE, false);
291 * @param headerMessage The message displayed at the top.
292 * @param leftMode The mode of the left button.
293 * @param rightMode The mode of the right button.
294 * @param footerMessage The footer message.
295 * @param patternEnabled Whether the pattern widget is enabled.
297 Stage(int headerMessage,
298 LeftButtonMode leftMode,
299 RightButtonMode rightMode,
300 int footerMessage, boolean patternEnabled) {
301 this.headerMessage = headerMessage;
302 this.leftMode = leftMode;
303 this.rightMode = rightMode;
304 this.footerMessage = footerMessage;
305 this.patternEnabled = patternEnabled;
308 final int headerMessage;
309 final LeftButtonMode leftMode;
310 final RightButtonMode rightMode;
311 final int footerMessage;
312 final boolean patternEnabled;
315 private Stage mUiStage = Stage.Introduction;
316 private boolean mDone = false;
318 private Runnable mClearPatternRunnable = new Runnable() {
320 mLockPatternView.clearPattern();
324 private ChooseLockSettingsHelper mChooseLockSettingsHelper;
326 private static final String KEY_UI_STAGE = "uiStage";
327 private static final String KEY_PATTERN_CHOICE = "chosenPattern";
330 public void onCreate(Bundle savedInstanceState) {
331 super.onCreate(savedInstanceState);
332 mChooseLockSettingsHelper = new ChooseLockSettingsHelper(getActivity());
333 if (!(getActivity() instanceof ChooseLockPattern)) {
334 throw new SecurityException("Fragment contained in wrong activity");
339 public View onCreateView(LayoutInflater inflater, ViewGroup container,
340 Bundle savedInstanceState) {
341 return inflater.inflate(R.layout.choose_lock_pattern, container, false);
345 public void onViewCreated(View view, Bundle savedInstanceState) {
346 super.onViewCreated(view, savedInstanceState);
347 mHeaderText = (TextView) view.findViewById(R.id.headerText);
348 mLockPatternView = (LockPatternView) view.findViewById(R.id.lockPattern);
349 mLockPatternView.setOnPatternListener(mChooseNewLockPatternListener);
350 mLockPatternView.setTactileFeedbackEnabled(
351 mChooseLockSettingsHelper.utils().isTactileFeedbackEnabled());
353 mFooterText = (TextView) view.findViewById(R.id.footerText);
355 mFooterLeftButton = (TextView) view.findViewById(R.id.footerLeftButton);
356 mFooterRightButton = (TextView) view.findViewById(R.id.footerRightButton);
358 mFooterLeftButton.setOnClickListener(this);
359 mFooterRightButton.setOnClickListener(this);
361 // make it so unhandled touch events within the unlock screen go to the
362 // lock pattern view.
363 final LinearLayoutWithDefaultTouchRecepient topLayout
364 = (LinearLayoutWithDefaultTouchRecepient) view.findViewById(
366 topLayout.setDefaultTouchRecepient(mLockPatternView);
368 final boolean confirmCredentials = getActivity().getIntent()
369 .getBooleanExtra("confirm_credentials", true);
371 if (savedInstanceState == null) {
372 if (confirmCredentials) {
373 // first launch. As a security measure, we're in NeedToConfirm mode until we
374 // know there isn't an existing password or the user confirms their password.
375 updateStage(Stage.NeedToConfirm);
376 boolean launchedConfirmationActivity =
377 mChooseLockSettingsHelper.launchConfirmationActivity(
378 CONFIRM_EXISTING_REQUEST, null, null);
379 if (!launchedConfirmationActivity) {
380 updateStage(Stage.Introduction);
383 updateStage(Stage.Introduction);
386 // restore from previous state
387 final String patternString = savedInstanceState.getString(KEY_PATTERN_CHOICE);
388 if (patternString != null) {
389 mChosenPattern = LockPatternUtils.stringToPattern(patternString);
391 updateStage(Stage.values()[savedInstanceState.getInt(KEY_UI_STAGE)]);
396 protected Intent getRedactionInterstitialIntent(Context context) {
397 return RedactionInterstitial.createStartIntent(context);
400 public void handleLeftButton() {
401 if (mUiStage.leftMode == LeftButtonMode.Retry) {
402 mChosenPattern = null;
403 mLockPatternView.clearPattern();
404 updateStage(Stage.Introduction);
405 } else if (mUiStage.leftMode == LeftButtonMode.Cancel) {
406 // They are canceling the entire wizard
407 getActivity().setResult(RESULT_FINISHED);
408 getActivity().finish();
410 throw new IllegalStateException("left footer button pressed, but stage of " +
411 mUiStage + " doesn't make sense");
415 public void handleRightButton() {
416 if (mUiStage.rightMode == RightButtonMode.Continue) {
417 if (mUiStage != Stage.FirstChoiceValid) {
418 throw new IllegalStateException("expected ui stage "
419 + Stage.FirstChoiceValid + " when button is "
420 + RightButtonMode.Continue);
422 updateStage(Stage.NeedToConfirm);
423 } else if (mUiStage.rightMode == RightButtonMode.Confirm) {
424 if (mUiStage != Stage.ChoiceConfirmed) {
425 throw new IllegalStateException("expected ui stage " + Stage.ChoiceConfirmed
426 + " when button is " + RightButtonMode.Confirm);
428 saveChosenPatternAndFinish();
429 } else if (mUiStage.rightMode == RightButtonMode.Ok) {
430 if (mUiStage != Stage.HelpScreen) {
431 throw new IllegalStateException("Help screen is only mode with ok button, "
432 + "but stage is " + mUiStage);
434 mLockPatternView.clearPattern();
435 mLockPatternView.setDisplayMode(DisplayMode.Correct);
436 updateStage(Stage.Introduction);
440 public void onClick(View v) {
441 if (v == mFooterLeftButton) {
443 } else if (v == mFooterRightButton) {
448 public boolean onKeyDown(int keyCode, KeyEvent event) {
449 if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
450 if (mUiStage == Stage.HelpScreen) {
451 updateStage(Stage.Introduction);
455 if (keyCode == KeyEvent.KEYCODE_MENU && mUiStage == Stage.Introduction) {
456 updateStage(Stage.HelpScreen);
462 public void onSaveInstanceState(Bundle outState) {
463 super.onSaveInstanceState(outState);
465 outState.putInt(KEY_UI_STAGE, mUiStage.ordinal());
466 if (mChosenPattern != null) {
467 outState.putString(KEY_PATTERN_CHOICE,
468 LockPatternUtils.patternToString(mChosenPattern));
473 * Updates the messages and buttons appropriate to what stage the user
474 * is at in choosing a view. This doesn't handle clearing out the pattern;
475 * the pattern is expected to be in the right state.
478 protected void updateStage(Stage stage) {
479 final Stage previousStage = mUiStage;
483 // header text, footer text, visibility and
484 // enabled state all known from the stage
485 if (stage == Stage.ChoiceTooShort) {
487 getResources().getString(
489 LockPatternUtils.MIN_LOCK_PATTERN_SIZE));
491 mHeaderText.setText(stage.headerMessage);
493 if (stage.footerMessage == ID_EMPTY_MESSAGE) {
494 mFooterText.setText("");
496 mFooterText.setText(stage.footerMessage);
499 if (stage.leftMode == LeftButtonMode.Gone) {
500 mFooterLeftButton.setVisibility(View.GONE);
502 mFooterLeftButton.setVisibility(View.VISIBLE);
503 mFooterLeftButton.setText(stage.leftMode.text);
504 mFooterLeftButton.setEnabled(stage.leftMode.enabled);
507 setRightButtonText(stage.rightMode.text);
508 setRightButtonEnabled(stage.rightMode.enabled);
510 // same for whether the patten is enabled
511 if (stage.patternEnabled) {
512 mLockPatternView.enableInput();
514 mLockPatternView.disableInput();
517 // the rest of the stuff varies enough that it is easier just to handle
518 // on a case by case basis.
519 mLockPatternView.setDisplayMode(DisplayMode.Correct);
523 mLockPatternView.clearPattern();
526 mLockPatternView.setPattern(DisplayMode.Animate, mAnimatePattern);
529 mLockPatternView.setDisplayMode(DisplayMode.Wrong);
530 postClearPatternRunnable();
532 case FirstChoiceValid:
535 mLockPatternView.clearPattern();
538 mLockPatternView.setDisplayMode(DisplayMode.Wrong);
539 postClearPatternRunnable();
541 case ChoiceConfirmed:
545 // If the stage changed, announce the header for accessibility. This
546 // is a no-op when accessibility is disabled.
547 if (previousStage != stage) {
548 mHeaderText.announceForAccessibility(mHeaderText.getText());
553 // clear the wrong pattern unless they have started a new one
555 private void postClearPatternRunnable() {
556 mLockPatternView.removeCallbacks(mClearPatternRunnable);
557 mLockPatternView.postDelayed(mClearPatternRunnable, WRONG_PATTERN_CLEAR_TIMEOUT_MS);
560 private void saveChosenPatternAndFinish() {
562 LockPatternUtils utils = mChooseLockSettingsHelper.utils();
563 final boolean lockVirgin = !utils.isPatternEverChosen();
565 boolean wasSecureBefore = utils.isSecure();
567 final boolean required = getActivity().getIntent().getBooleanExtra(
568 EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true);
569 utils.setCredentialRequiredToDecrypt(required);
570 utils.setLockPatternEnabled(true);
571 utils.saveLockPattern(mChosenPattern);
574 utils.setVisiblePatternEnabled(true);
577 if (!wasSecureBefore) {
578 startActivity(getRedactionInterstitialIntent(getActivity()));
580 getActivity().setResult(RESULT_FINISHED);
581 getActivity().finish();