1 /* 2 * Copyright (C) 2010 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.android.internal.widget.LockPatternUtils; 20 import com.android.internal.widget.PasswordEntryKeyboardHelper; 21 import com.android.internal.widget.PasswordEntryKeyboardView; 22 23 import android.app.Activity; 24 import android.app.Fragment; 25 import android.app.admin.DevicePolicyManager; 26 import android.content.Intent; 27 import android.inputmethodservice.KeyboardView; 28 import android.os.Bundle; 29 import android.os.Handler; 30 import android.os.Message; 31 import android.preference.PreferenceActivity; 32 import android.text.Editable; 33 import android.text.InputType; 34 import android.text.Selection; 35 import android.text.Spannable; 36 import android.text.TextUtils; 37 import android.text.TextWatcher; 38 import android.view.KeyEvent; 39 import android.view.LayoutInflater; 40 import android.view.View; 41 import android.view.ViewGroup; 42 import android.view.View.OnClickListener; 43 import android.view.accessibility.AccessibilityEvent; 44 import android.view.inputmethod.EditorInfo; 45 import android.widget.Button; 46 import android.widget.TextView; 47 import android.widget.TextView.OnEditorActionListener; 48 49 public class ChooseLockPassword extends PreferenceActivity { 50 public static final String PASSWORD_MIN_KEY = "lockscreen.password_min"; 51 public static final String PASSWORD_MAX_KEY = "lockscreen.password_max"; 52 public static final String PASSWORD_MIN_LETTERS_KEY = "lockscreen.password_min_letters"; 53 public static final String PASSWORD_MIN_LOWERCASE_KEY = "lockscreen.password_min_lowercase"; 54 public static final String PASSWORD_MIN_UPPERCASE_KEY = "lockscreen.password_min_uppercase"; 55 public static final String PASSWORD_MIN_NUMERIC_KEY = "lockscreen.password_min_numeric"; 56 public static final String PASSWORD_MIN_SYMBOLS_KEY = "lockscreen.password_min_symbols"; 57 public static final String PASSWORD_MIN_NONLETTER_KEY = "lockscreen.password_min_nonletter"; 58 59 @Override getIntent()60 public Intent getIntent() { 61 Intent modIntent = new Intent(super.getIntent()); 62 modIntent.putExtra(EXTRA_SHOW_FRAGMENT, ChooseLockPasswordFragment.class.getName()); 63 modIntent.putExtra(EXTRA_NO_HEADERS, true); 64 return modIntent; 65 } 66 67 @Override onCreate(Bundle savedInstanceState)68 public void onCreate(Bundle savedInstanceState) { 69 // TODO: Fix on phones 70 // Disable IME on our window since we provide our own keyboard 71 //getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 72 //WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 73 super.onCreate(savedInstanceState); 74 CharSequence msg = getText(R.string.lockpassword_choose_your_password_header); 75 showBreadCrumbs(msg, msg); 76 } 77 78 public static class ChooseLockPasswordFragment extends Fragment 79 implements OnClickListener, OnEditorActionListener, TextWatcher { 80 private static final String KEY_FIRST_PIN = "first_pin"; 81 private static final String KEY_UI_STAGE = "ui_stage"; 82 private TextView mPasswordEntry; 83 private int mPasswordMinLength = 4; 84 private int mPasswordMaxLength = 16; 85 private int mPasswordMinLetters = 0; 86 private int mPasswordMinUpperCase = 0; 87 private int mPasswordMinLowerCase = 0; 88 private int mPasswordMinSymbols = 0; 89 private int mPasswordMinNumeric = 0; 90 private int mPasswordMinNonLetter = 0; 91 private LockPatternUtils mLockPatternUtils; 92 private int mRequestedQuality = DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; 93 private ChooseLockSettingsHelper mChooseLockSettingsHelper; 94 private Stage mUiStage = Stage.Introduction; 95 private TextView mHeaderText; 96 private String mFirstPin; 97 private KeyboardView mKeyboardView; 98 private PasswordEntryKeyboardHelper mKeyboardHelper; 99 private boolean mIsAlphaMode; 100 private Button mCancelButton; 101 private Button mNextButton; 102 private static final int CONFIRM_EXISTING_REQUEST = 58; 103 static final int RESULT_FINISHED = RESULT_FIRST_USER; 104 private static final long ERROR_MESSAGE_TIMEOUT = 3000; 105 private static final int MSG_SHOW_ERROR = 1; 106 107 private Handler mHandler = new Handler() { 108 @Override 109 public void handleMessage(Message msg) { 110 if (msg.what == MSG_SHOW_ERROR) { 111 updateStage((Stage) msg.obj); 112 } 113 } 114 }; 115 116 /** 117 * Keep track internally of where the user is in choosing a pattern. 118 */ 119 protected enum Stage { 120 121 Introduction(R.string.lockpassword_choose_your_password_header, 122 R.string.lockpassword_choose_your_pin_header, 123 R.string.lockpassword_continue_label), 124 125 NeedToConfirm(R.string.lockpassword_confirm_your_password_header, 126 R.string.lockpassword_confirm_your_pin_header, 127 R.string.lockpassword_ok_label), 128 129 ConfirmWrong(R.string.lockpassword_confirm_passwords_dont_match, 130 R.string.lockpassword_confirm_pins_dont_match, 131 R.string.lockpassword_continue_label); 132 133 /** 134 * @param headerMessage The message displayed at the top. 135 */ Stage(int hintInAlpha, int hintInNumeric, int nextButtonText)136 Stage(int hintInAlpha, int hintInNumeric, int nextButtonText) { 137 this.alphaHint = hintInAlpha; 138 this.numericHint = hintInNumeric; 139 this.buttonText = nextButtonText; 140 } 141 142 public final int alphaHint; 143 public final int numericHint; 144 public final int buttonText; 145 } 146 147 // required constructor for fragments ChooseLockPasswordFragment()148 public ChooseLockPasswordFragment() { 149 150 } 151 152 @Override onCreate(Bundle savedInstanceState)153 public void onCreate(Bundle savedInstanceState) { 154 super.onCreate(savedInstanceState); 155 mLockPatternUtils = new LockPatternUtils(getActivity()); 156 Intent intent = getActivity().getIntent(); 157 mRequestedQuality = Math.max(intent.getIntExtra(LockPatternUtils.PASSWORD_TYPE_KEY, 158 mRequestedQuality), mLockPatternUtils.getRequestedPasswordQuality()); 159 mPasswordMinLength = Math.max( 160 intent.getIntExtra(PASSWORD_MIN_KEY, mPasswordMinLength), mLockPatternUtils 161 .getRequestedMinimumPasswordLength()); 162 mPasswordMaxLength = intent.getIntExtra(PASSWORD_MAX_KEY, mPasswordMaxLength); 163 mPasswordMinLetters = Math.max(intent.getIntExtra(PASSWORD_MIN_LETTERS_KEY, 164 mPasswordMinLetters), mLockPatternUtils.getRequestedPasswordMinimumLetters()); 165 mPasswordMinUpperCase = Math.max(intent.getIntExtra(PASSWORD_MIN_UPPERCASE_KEY, 166 mPasswordMinUpperCase), mLockPatternUtils.getRequestedPasswordMinimumUpperCase()); 167 mPasswordMinLowerCase = Math.max(intent.getIntExtra(PASSWORD_MIN_LOWERCASE_KEY, 168 mPasswordMinLowerCase), mLockPatternUtils.getRequestedPasswordMinimumLowerCase()); 169 mPasswordMinNumeric = Math.max(intent.getIntExtra(PASSWORD_MIN_NUMERIC_KEY, 170 mPasswordMinNumeric), mLockPatternUtils.getRequestedPasswordMinimumNumeric()); 171 mPasswordMinSymbols = Math.max(intent.getIntExtra(PASSWORD_MIN_SYMBOLS_KEY, 172 mPasswordMinSymbols), mLockPatternUtils.getRequestedPasswordMinimumSymbols()); 173 mPasswordMinNonLetter = Math.max(intent.getIntExtra(PASSWORD_MIN_NONLETTER_KEY, 174 mPasswordMinNonLetter), mLockPatternUtils.getRequestedPasswordMinimumNonLetter()); 175 176 mChooseLockSettingsHelper = new ChooseLockSettingsHelper(getActivity()); 177 } 178 179 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)180 public View onCreateView(LayoutInflater inflater, ViewGroup container, 181 Bundle savedInstanceState) { 182 183 View view = inflater.inflate(R.layout.choose_lock_password, null); 184 185 mCancelButton = (Button) view.findViewById(R.id.cancel_button); 186 mCancelButton.setOnClickListener(this); 187 mNextButton = (Button) view.findViewById(R.id.next_button); 188 mNextButton.setOnClickListener(this); 189 190 mIsAlphaMode = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == mRequestedQuality 191 || DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == mRequestedQuality 192 || DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == mRequestedQuality; 193 mKeyboardView = (PasswordEntryKeyboardView) view.findViewById(R.id.keyboard); 194 mPasswordEntry = (TextView) view.findViewById(R.id.password_entry); 195 mPasswordEntry.setOnEditorActionListener(this); 196 mPasswordEntry.addTextChangedListener(this); 197 198 final Activity activity = getActivity(); 199 mKeyboardHelper = new PasswordEntryKeyboardHelper(activity, 200 mKeyboardView, mPasswordEntry); 201 mKeyboardHelper.setKeyboardMode(mIsAlphaMode ? 202 PasswordEntryKeyboardHelper.KEYBOARD_MODE_ALPHA 203 : PasswordEntryKeyboardHelper.KEYBOARD_MODE_NUMERIC); 204 205 mHeaderText = (TextView) view.findViewById(R.id.headerText); 206 mKeyboardView.requestFocus(); 207 208 int currentType = mPasswordEntry.getInputType(); 209 mPasswordEntry.setInputType(mIsAlphaMode ? currentType 210 : (InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD)); 211 212 Intent intent = getActivity().getIntent(); 213 final boolean confirmCredentials = intent.getBooleanExtra("confirm_credentials", true); 214 if (savedInstanceState == null) { 215 updateStage(Stage.Introduction); 216 if (confirmCredentials) { 217 mChooseLockSettingsHelper.launchConfirmationActivity(CONFIRM_EXISTING_REQUEST, 218 null, null); 219 } 220 } else { 221 mFirstPin = savedInstanceState.getString(KEY_FIRST_PIN); 222 final String state = savedInstanceState.getString(KEY_UI_STAGE); 223 if (state != null) { 224 mUiStage = Stage.valueOf(state); 225 updateStage(mUiStage); 226 } 227 } 228 // Update the breadcrumb (title) if this is embedded in a PreferenceActivity 229 if (activity instanceof PreferenceActivity) { 230 final PreferenceActivity preferenceActivity = (PreferenceActivity) activity; 231 int id = mIsAlphaMode ? R.string.lockpassword_choose_your_password_header 232 : R.string.lockpassword_choose_your_pin_header; 233 CharSequence title = getText(id); 234 preferenceActivity.showBreadCrumbs(title, title); 235 } 236 237 return view; 238 } 239 240 @Override onResume()241 public void onResume() { 242 super.onResume(); 243 updateStage(mUiStage); 244 mKeyboardView.requestFocus(); 245 } 246 247 @Override onPause()248 public void onPause() { 249 mHandler.removeMessages(MSG_SHOW_ERROR); 250 251 super.onPause(); 252 } 253 254 @Override onSaveInstanceState(Bundle outState)255 public void onSaveInstanceState(Bundle outState) { 256 super.onSaveInstanceState(outState); 257 outState.putString(KEY_UI_STAGE, mUiStage.name()); 258 outState.putString(KEY_FIRST_PIN, mFirstPin); 259 } 260 261 @Override onActivityResult(int requestCode, int resultCode, Intent data)262 public void onActivityResult(int requestCode, int resultCode, 263 Intent data) { 264 super.onActivityResult(requestCode, resultCode, data); 265 switch (requestCode) { 266 case CONFIRM_EXISTING_REQUEST: 267 if (resultCode != Activity.RESULT_OK) { 268 getActivity().setResult(RESULT_FINISHED); 269 getActivity().finish(); 270 } 271 break; 272 } 273 } 274 updateStage(Stage stage)275 protected void updateStage(Stage stage) { 276 final Stage previousStage = mUiStage; 277 mUiStage = stage; 278 updateUi(); 279 280 // If the stage changed, announce the header for accessibility. This 281 // is a no-op when accessibility is disabled. 282 if (previousStage != stage) { 283 mHeaderText.announceForAccessibility(mHeaderText.getText()); 284 } 285 } 286 287 /** 288 * Validates PIN and returns a message to display if PIN fails test. 289 * @param password the raw password the user typed in 290 * @return error message to show to user or null if password is OK 291 */ validatePassword(String password)292 private String validatePassword(String password) { 293 if (password.length() < mPasswordMinLength) { 294 return getString(mIsAlphaMode ? 295 R.string.lockpassword_password_too_short 296 : R.string.lockpassword_pin_too_short, mPasswordMinLength); 297 } 298 if (password.length() > mPasswordMaxLength) { 299 return getString(mIsAlphaMode ? 300 R.string.lockpassword_password_too_long 301 : R.string.lockpassword_pin_too_long, mPasswordMaxLength + 1); 302 } 303 int letters = 0; 304 int numbers = 0; 305 int lowercase = 0; 306 int symbols = 0; 307 int uppercase = 0; 308 int nonletter = 0; 309 for (int i = 0; i < password.length(); i++) { 310 char c = password.charAt(i); 311 // allow non control Latin-1 characters only 312 if (c < 32 || c > 127) { 313 return getString(R.string.lockpassword_illegal_character); 314 } 315 if (c >= '0' && c <= '9') { 316 numbers++; 317 nonletter++; 318 } else if (c >= 'A' && c <= 'Z') { 319 letters++; 320 uppercase++; 321 } else if (c >= 'a' && c <= 'z') { 322 letters++; 323 lowercase++; 324 } else { 325 symbols++; 326 nonletter++; 327 } 328 } 329 if (DevicePolicyManager.PASSWORD_QUALITY_NUMERIC == mRequestedQuality 330 && (letters > 0 || symbols > 0)) { 331 // This shouldn't be possible unless user finds some way to bring up 332 // soft keyboard 333 return getString(R.string.lockpassword_pin_contains_non_digits); 334 } else if (DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == mRequestedQuality) { 335 if (letters < mPasswordMinLetters) { 336 return String.format(getResources().getQuantityString( 337 R.plurals.lockpassword_password_requires_letters, mPasswordMinLetters), 338 mPasswordMinLetters); 339 } else if (numbers < mPasswordMinNumeric) { 340 return String.format(getResources().getQuantityString( 341 R.plurals.lockpassword_password_requires_numeric, mPasswordMinNumeric), 342 mPasswordMinNumeric); 343 } else if (lowercase < mPasswordMinLowerCase) { 344 return String.format(getResources().getQuantityString( 345 R.plurals.lockpassword_password_requires_lowercase, mPasswordMinLowerCase), 346 mPasswordMinLowerCase); 347 } else if (uppercase < mPasswordMinUpperCase) { 348 return String.format(getResources().getQuantityString( 349 R.plurals.lockpassword_password_requires_uppercase, mPasswordMinUpperCase), 350 mPasswordMinUpperCase); 351 } else if (symbols < mPasswordMinSymbols) { 352 return String.format(getResources().getQuantityString( 353 R.plurals.lockpassword_password_requires_symbols, mPasswordMinSymbols), 354 mPasswordMinSymbols); 355 } else if (nonletter < mPasswordMinNonLetter) { 356 return String.format(getResources().getQuantityString( 357 R.plurals.lockpassword_password_requires_nonletter, mPasswordMinNonLetter), 358 mPasswordMinNonLetter); 359 } 360 } else { 361 final boolean alphabetic = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC 362 == mRequestedQuality; 363 final boolean alphanumeric = DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC 364 == mRequestedQuality; 365 if ((alphabetic || alphanumeric) && letters == 0) { 366 return getString(R.string.lockpassword_password_requires_alpha); 367 } 368 if (alphanumeric && numbers == 0) { 369 return getString(R.string.lockpassword_password_requires_digit); 370 } 371 } 372 if(mLockPatternUtils.checkPasswordHistory(password)) { 373 return getString(mIsAlphaMode ? R.string.lockpassword_password_recently_used 374 : R.string.lockpassword_pin_recently_used); 375 } 376 return null; 377 } 378 handleNext()379 private void handleNext() { 380 final String pin = mPasswordEntry.getText().toString(); 381 if (TextUtils.isEmpty(pin)) { 382 return; 383 } 384 String errorMsg = null; 385 if (mUiStage == Stage.Introduction) { 386 errorMsg = validatePassword(pin); 387 if (errorMsg == null) { 388 mFirstPin = pin; 389 mPasswordEntry.setText(""); 390 updateStage(Stage.NeedToConfirm); 391 } 392 } else if (mUiStage == Stage.NeedToConfirm) { 393 if (mFirstPin.equals(pin)) { 394 final boolean isFallback = getActivity().getIntent().getBooleanExtra( 395 LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK, false); 396 mLockPatternUtils.clearLock(isFallback); 397 mLockPatternUtils.saveLockPassword(pin, mRequestedQuality, isFallback); 398 getActivity().setResult(RESULT_FINISHED); 399 getActivity().finish(); 400 } else { 401 CharSequence tmp = mPasswordEntry.getText(); 402 if (tmp != null) { 403 Selection.setSelection((Spannable) tmp, 0, tmp.length()); 404 } 405 updateStage(Stage.ConfirmWrong); 406 } 407 } 408 if (errorMsg != null) { 409 showError(errorMsg, mUiStage); 410 } 411 } 412 onClick(View v)413 public void onClick(View v) { 414 switch (v.getId()) { 415 case R.id.next_button: 416 handleNext(); 417 break; 418 419 case R.id.cancel_button: 420 getActivity().finish(); 421 break; 422 } 423 } 424 showError(String msg, final Stage next)425 private void showError(String msg, final Stage next) { 426 mHeaderText.setText(msg); 427 mHeaderText.announceForAccessibility(mHeaderText.getText()); 428 Message mesg = mHandler.obtainMessage(MSG_SHOW_ERROR, next); 429 mHandler.removeMessages(MSG_SHOW_ERROR); 430 mHandler.sendMessageDelayed(mesg, ERROR_MESSAGE_TIMEOUT); 431 } 432 onEditorAction(TextView v, int actionId, KeyEvent event)433 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 434 // Check if this was the result of hitting the enter or "done" key 435 if (actionId == EditorInfo.IME_NULL 436 || actionId == EditorInfo.IME_ACTION_DONE 437 || actionId == EditorInfo.IME_ACTION_NEXT) { 438 handleNext(); 439 return true; 440 } 441 return false; 442 } 443 444 /** 445 * Update the hint based on current Stage and length of password entry 446 */ updateUi()447 private void updateUi() { 448 String password = mPasswordEntry.getText().toString(); 449 final int length = password.length(); 450 if (mUiStage == Stage.Introduction && length > 0) { 451 if (length < mPasswordMinLength) { 452 String msg = getString(mIsAlphaMode ? R.string.lockpassword_password_too_short 453 : R.string.lockpassword_pin_too_short, mPasswordMinLength); 454 mHeaderText.setText(msg); 455 mNextButton.setEnabled(false); 456 } else { 457 String error = validatePassword(password); 458 if (error != null) { 459 mHeaderText.setText(error); 460 mNextButton.setEnabled(false); 461 } else { 462 mHeaderText.setText(R.string.lockpassword_press_continue); 463 mNextButton.setEnabled(true); 464 } 465 } 466 } else { 467 mHeaderText.setText(mIsAlphaMode ? mUiStage.alphaHint : mUiStage.numericHint); 468 mNextButton.setEnabled(length > 0); 469 } 470 mNextButton.setText(mUiStage.buttonText); 471 } 472 afterTextChanged(Editable s)473 public void afterTextChanged(Editable s) { 474 // Changing the text while error displayed resets to NeedToConfirm state 475 if (mUiStage == Stage.ConfirmWrong) { 476 mUiStage = Stage.NeedToConfirm; 477 } 478 updateUi(); 479 } 480 beforeTextChanged(CharSequence s, int start, int count, int after)481 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 482 483 } 484 onTextChanged(CharSequence s, int start, int before, int count)485 public void onTextChanged(CharSequence s, int start, int before, int count) { 486 487 } 488 } 489 } 490