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