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