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 android.app.Activity; 20 import android.app.Fragment; 21 import android.app.admin.DevicePolicyManager; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.graphics.drawable.InsetDrawable; 25 import android.os.Bundle; 26 import android.os.Handler; 27 import android.os.Message; 28 import android.support.v7.widget.LinearLayoutManager; 29 import android.support.v7.widget.RecyclerView; 30 import android.text.Editable; 31 import android.text.InputType; 32 import android.text.Selection; 33 import android.text.Spannable; 34 import android.text.TextUtils; 35 import android.text.TextWatcher; 36 import android.util.Log; 37 import android.view.inputmethod.EditorInfo; 38 import android.view.KeyEvent; 39 import android.view.LayoutInflater; 40 import android.view.View; 41 import android.view.View.OnClickListener; 42 import android.view.ViewGroup; 43 import android.widget.Button; 44 import android.widget.EditText; 45 import android.widget.LinearLayout; 46 import android.widget.TextView; 47 import android.widget.TextView.OnEditorActionListener; 48 49 import com.android.internal.logging.MetricsProto.MetricsEvent; 50 import com.android.internal.widget.LockPatternUtils; 51 import com.android.internal.widget.LockPatternUtils.RequestThrottledException; 52 import com.android.internal.widget.TextViewInputDisabler; 53 import com.android.settings.notification.RedactionInterstitial; 54 import com.android.settings.password.PasswordRequirementAdapter; 55 import com.android.setupwizardlib.GlifLayout; 56 57 import java.util.ArrayList; 58 import java.util.List; 59 60 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC; 61 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC; 62 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; 63 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; 64 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX; 65 66 public class ChooseLockPassword extends SettingsActivity { 67 public static final String PASSWORD_MIN_KEY = "lockscreen.password_min"; 68 public static final String PASSWORD_MAX_KEY = "lockscreen.password_max"; 69 public static final String PASSWORD_MIN_LETTERS_KEY = "lockscreen.password_min_letters"; 70 public static final String PASSWORD_MIN_LOWERCASE_KEY = "lockscreen.password_min_lowercase"; 71 public static final String PASSWORD_MIN_UPPERCASE_KEY = "lockscreen.password_min_uppercase"; 72 public static final String PASSWORD_MIN_NUMERIC_KEY = "lockscreen.password_min_numeric"; 73 public static final String PASSWORD_MIN_SYMBOLS_KEY = "lockscreen.password_min_symbols"; 74 public static final String PASSWORD_MIN_NONLETTER_KEY = "lockscreen.password_min_nonletter"; 75 76 private static final String TAG = "ChooseLockPassword"; 77 78 @Override 79 public Intent getIntent() { 80 Intent modIntent = new Intent(super.getIntent()); 81 modIntent.putExtra(EXTRA_SHOW_FRAGMENT, getFragmentClass().getName()); 82 return modIntent; 83 } 84 85 public static Intent createIntent(Context context, int quality, 86 int minLength, final int maxLength, boolean requirePasswordToDecrypt, 87 boolean confirmCredentials) { 88 Intent intent = new Intent().setClass(context, ChooseLockPassword.class); 89 intent.putExtra(LockPatternUtils.PASSWORD_TYPE_KEY, quality); 90 intent.putExtra(PASSWORD_MIN_KEY, minLength); 91 intent.putExtra(PASSWORD_MAX_KEY, maxLength); 92 intent.putExtra(ChooseLockGeneric.CONFIRM_CREDENTIALS, confirmCredentials); 93 intent.putExtra(EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, requirePasswordToDecrypt); 94 return intent; 95 } 96 97 public static Intent createIntent(Context context, int quality, 98 int minLength, final int maxLength, boolean requirePasswordToDecrypt, 99 boolean confirmCredentials, int userId) { 100 Intent intent = createIntent(context, quality, minLength, maxLength, 101 requirePasswordToDecrypt, confirmCredentials); 102 intent.putExtra(Intent.EXTRA_USER_ID, userId); 103 return intent; 104 } 105 106 public static Intent createIntent(Context context, int quality, 107 int minLength, final int maxLength, boolean requirePasswordToDecrypt, String password) { 108 Intent intent = createIntent(context, quality, minLength, maxLength, 109 requirePasswordToDecrypt, false); 110 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, password); 111 return intent; 112 } 113 114 public static Intent createIntent(Context context, int quality, int minLength, 115 int maxLength, boolean requirePasswordToDecrypt, String password, int userId) { 116 Intent intent = createIntent(context, quality, minLength, maxLength, 117 requirePasswordToDecrypt, password); 118 intent.putExtra(Intent.EXTRA_USER_ID, userId); 119 return intent; 120 } 121 122 public static Intent createIntent(Context context, int quality, 123 int minLength, final int maxLength, boolean requirePasswordToDecrypt, long challenge) { 124 Intent intent = createIntent(context, quality, minLength, maxLength, 125 requirePasswordToDecrypt, false); 126 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, true); 127 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, challenge); 128 return intent; 129 } 130 131 public static Intent createIntent(Context context, int quality, int minLength, 132 int maxLength, boolean requirePasswordToDecrypt, long challenge, int userId) { 133 Intent intent = createIntent(context, quality, minLength, maxLength, 134 requirePasswordToDecrypt, challenge); 135 intent.putExtra(Intent.EXTRA_USER_ID, userId); 136 return intent; 137 } 138 139 @Override 140 protected boolean isValidFragment(String fragmentName) { 141 if (ChooseLockPasswordFragment.class.getName().equals(fragmentName)) return true; 142 return false; 143 } 144 145 /* package */ Class<? extends Fragment> getFragmentClass() { 146 return ChooseLockPasswordFragment.class; 147 } 148 149 @Override 150 protected void onCreate(Bundle savedInstanceState) { 151 super.onCreate(savedInstanceState); 152 CharSequence msg = getText(R.string.lockpassword_choose_your_password_header); 153 setTitle(msg); 154 LinearLayout layout = (LinearLayout) findViewById(R.id.content_parent); 155 layout.setFitsSystemWindows(false); 156 } 157 158 public static class ChooseLockPasswordFragment extends InstrumentedFragment 159 implements OnClickListener, OnEditorActionListener, TextWatcher, 160 SaveAndFinishWorker.Listener { 161 private static final String KEY_FIRST_PIN = "first_pin"; 162 private static final String KEY_UI_STAGE = "ui_stage"; 163 private static final String KEY_CURRENT_PASSWORD = "current_password"; 164 private static final String FRAGMENT_TAG_SAVE_AND_FINISH = "save_and_finish_worker"; 165 166 private String mCurrentPassword; 167 private String mChosenPassword; 168 private boolean mHasChallenge; 169 private long mChallenge; 170 private EditText mPasswordEntry; 171 private TextViewInputDisabler mPasswordEntryInputDisabler; 172 private int mPasswordMinLength = LockPatternUtils.MIN_LOCK_PASSWORD_SIZE; 173 private int mPasswordMaxLength = 16; 174 private int mPasswordMinLetters = 0; 175 private int mPasswordMinUpperCase = 0; 176 private int mPasswordMinLowerCase = 0; 177 private int mPasswordMinSymbols = 0; 178 private int mPasswordMinNumeric = 0; 179 private int mPasswordMinNonLetter = 0; 180 private int mPasswordMinLengthToFulfillAllPolicies = 0; 181 private int mUserId; 182 private boolean mHideDrawer = false; 183 /** 184 * Password requirements that we need to verify. 185 */ 186 private int[] mPasswordRequirements; 187 188 private LockPatternUtils mLockPatternUtils; 189 private SaveAndFinishWorker mSaveAndFinishWorker; 190 private int mRequestedQuality = DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; 191 private ChooseLockSettingsHelper mChooseLockSettingsHelper; 192 private Stage mUiStage = Stage.Introduction; 193 private PasswordRequirementAdapter mPasswordRequirementAdapter; 194 195 private TextView mHeaderText; 196 private String mFirstPin; 197 private RecyclerView mPasswordRestrictionView; 198 private boolean mIsAlphaMode; 199 private Button mCancelButton; 200 private Button mNextButton; 201 202 private TextChangedHandler mTextChangedHandler; 203 204 private static final int CONFIRM_EXISTING_REQUEST = 58; 205 static final int RESULT_FINISHED = RESULT_FIRST_USER; 206 207 private static final int MIN_LETTER_IN_PASSWORD = 0; 208 private static final int MIN_UPPER_LETTERS_IN_PASSWORD = 1; 209 private static final int MIN_LOWER_LETTERS_IN_PASSWORD = 2; 210 private static final int MIN_SYMBOLS_IN_PASSWORD = 3; 211 private static final int MIN_NUMBER_IN_PASSWORD = 4; 212 private static final int MIN_NON_LETTER_IN_PASSWORD = 5; 213 214 // Error code returned from {@link #validatePassword(String)}. 215 private static final int NO_ERROR = 0; 216 private static final int CONTAIN_INVALID_CHARACTERS = 1 << 0; 217 private static final int TOO_SHORT = 1 << 1; 218 private static final int TOO_LONG = 1 << 2; 219 private static final int CONTAIN_NON_DIGITS = 1 << 3; 220 private static final int CONTAIN_SEQUENTIAL_DIGITS = 1 << 4; 221 private static final int RECENTLY_USED = 1 << 5; 222 private static final int NOT_ENOUGH_LETTER = 1 << 6; 223 private static final int NOT_ENOUGH_UPPER_CASE = 1 << 7; 224 private static final int NOT_ENOUGH_LOWER_CASE = 1 << 8; 225 private static final int NOT_ENOUGH_DIGITS = 1 << 9; 226 private static final int NOT_ENOUGH_SYMBOLS = 1 << 10; 227 private static final int NOT_ENOUGH_NON_LETTER = 1 << 11; 228 229 /** 230 * Keep track internally of where the user is in choosing a pattern. 231 */ 232 protected enum Stage { 233 234 Introduction(R.string.lockpassword_choose_your_password_header, 235 R.string.lockpassword_choose_your_pin_header, 236 R.string.lockpassword_continue_label), 237 238 NeedToConfirm(R.string.lockpassword_confirm_your_password_header, 239 R.string.lockpassword_confirm_your_pin_header, 240 R.string.lockpassword_ok_label), 241 242 ConfirmWrong(R.string.lockpassword_confirm_passwords_dont_match, 243 R.string.lockpassword_confirm_pins_dont_match, 244 R.string.lockpassword_continue_label); 245 246 Stage(int hintInAlpha, int hintInNumeric, int nextButtonText) { 247 this.alphaHint = hintInAlpha; 248 this.numericHint = hintInNumeric; 249 this.buttonText = nextButtonText; 250 } 251 252 public final int alphaHint; 253 public final int numericHint; 254 public final int buttonText; 255 } 256 257 // required constructor for fragments 258 public ChooseLockPasswordFragment() { 259 260 } 261 262 @Override 263 public void onCreate(Bundle savedInstanceState) { 264 super.onCreate(savedInstanceState); 265 mLockPatternUtils = new LockPatternUtils(getActivity()); 266 Intent intent = getActivity().getIntent(); 267 if (!(getActivity() instanceof ChooseLockPassword)) { 268 throw new SecurityException("Fragment contained in wrong activity"); 269 } 270 // Only take this argument into account if it belongs to the current profile. 271 mUserId = Utils.getUserIdFromBundle(getActivity(), intent.getExtras()); 272 processPasswordRequirements(intent); 273 mChooseLockSettingsHelper = new ChooseLockSettingsHelper(getActivity()); 274 mHideDrawer = getActivity().getIntent().getBooleanExtra(EXTRA_HIDE_DRAWER, false); 275 276 if (intent.getBooleanExtra( 277 ChooseLockSettingsHelper.EXTRA_KEY_FOR_CHANGE_CRED_REQUIRED_FOR_BOOT, false)) { 278 SaveAndFinishWorker w = new SaveAndFinishWorker(); 279 final boolean required = getActivity().getIntent().getBooleanExtra( 280 EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true); 281 String current = intent.getStringExtra( 282 ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); 283 w.setBlocking(true); 284 w.setListener(this); 285 w.start(mChooseLockSettingsHelper.utils(), required, 286 false, 0, current, current, mRequestedQuality, mUserId); 287 } 288 mTextChangedHandler = new TextChangedHandler(); 289 } 290 291 @Override 292 public View onCreateView(LayoutInflater inflater, ViewGroup container, 293 Bundle savedInstanceState) { 294 return inflater.inflate(R.layout.choose_lock_password, container, false); 295 } 296 297 @Override 298 public void onViewCreated(View view, Bundle savedInstanceState) { 299 super.onViewCreated(view, savedInstanceState); 300 301 mCancelButton = (Button) view.findViewById(R.id.cancel_button); 302 mCancelButton.setOnClickListener(this); 303 mNextButton = (Button) view.findViewById(R.id.next_button); 304 mNextButton.setOnClickListener(this); 305 306 mIsAlphaMode = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == mRequestedQuality 307 || DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == mRequestedQuality 308 || DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == mRequestedQuality; 309 310 setupPasswordRequirementsView(view); 311 312 mPasswordRestrictionView.setLayoutManager(new LinearLayoutManager(getActivity())); 313 mPasswordEntry = (EditText) view.findViewById(R.id.password_entry); 314 mPasswordEntry.setOnEditorActionListener(this); 315 mPasswordEntry.addTextChangedListener(this); 316 mPasswordEntry.requestFocus(); 317 mPasswordEntryInputDisabler = new TextViewInputDisabler(mPasswordEntry); 318 319 final Activity activity = getActivity(); 320 mHeaderText = (TextView) view.findViewById(R.id.headerText); 321 322 int currentType = mPasswordEntry.getInputType(); 323 mPasswordEntry.setInputType(mIsAlphaMode ? currentType 324 : (InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD)); 325 326 Intent intent = getActivity().getIntent(); 327 final boolean confirmCredentials = intent.getBooleanExtra( 328 ChooseLockGeneric.CONFIRM_CREDENTIALS, true); 329 mCurrentPassword = intent.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); 330 mHasChallenge = intent.getBooleanExtra( 331 ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, false); 332 mChallenge = intent.getLongExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, 0); 333 if (savedInstanceState == null) { 334 updateStage(Stage.Introduction); 335 if (confirmCredentials) { 336 mChooseLockSettingsHelper.launchConfirmationActivity(CONFIRM_EXISTING_REQUEST, 337 getString(R.string.unlock_set_unlock_launch_picker_title), true, 338 mUserId); 339 } 340 } else { 341 // restore from previous state 342 mFirstPin = savedInstanceState.getString(KEY_FIRST_PIN); 343 final String state = savedInstanceState.getString(KEY_UI_STAGE); 344 if (state != null) { 345 mUiStage = Stage.valueOf(state); 346 updateStage(mUiStage); 347 } 348 349 if (mCurrentPassword == null) { 350 mCurrentPassword = savedInstanceState.getString(KEY_CURRENT_PASSWORD); 351 } 352 353 // Re-attach to the exiting worker if there is one. 354 mSaveAndFinishWorker = (SaveAndFinishWorker) getFragmentManager().findFragmentByTag( 355 FRAGMENT_TAG_SAVE_AND_FINISH); 356 } 357 358 // Workaround to show one password requirement below EditText when IME is shown. 359 // By adding an inset to the edit text background, we make the EditText occupy more 360 // vertical space, and the keyboard will then avoid hiding it. We have also set 361 // negative margin in the layout below in order to have them show in the correct 362 // position. 363 final int visibleVerticalSpaceBelowPassword = 364 getResources().getDimensionPixelOffset( 365 R.dimen.visible_vertical_space_below_password); 366 InsetDrawable drawable = 367 new InsetDrawable( 368 mPasswordEntry.getBackground(), 0, 0, 0, visibleVerticalSpaceBelowPassword); 369 mPasswordEntry.setBackgroundDrawable(drawable); 370 LinearLayout bottomContainer = (LinearLayout) view.findViewById(R.id.bottom_container); 371 LinearLayout.LayoutParams bottomContainerLp = 372 (LinearLayout.LayoutParams) bottomContainer.getLayoutParams(); 373 bottomContainerLp.setMargins(0, -visibleVerticalSpaceBelowPassword, 0, 0); 374 375 if (activity instanceof SettingsActivity) { 376 final SettingsActivity sa = (SettingsActivity) activity; 377 int id = mIsAlphaMode ? R.string.lockpassword_choose_your_password_header 378 : R.string.lockpassword_choose_your_pin_header; 379 CharSequence title = getText(id); 380 sa.setTitle(title); 381 ((GlifLayout) view).setHeaderText(title); 382 } 383 } 384 385 private void setupPasswordRequirementsView(View view) { 386 // Construct passwordRequirements and requirementDescriptions. 387 List<Integer> passwordRequirements = new ArrayList<>(); 388 List<String> requirementDescriptions = new ArrayList<>(); 389 if (mPasswordMinUpperCase > 0) { 390 passwordRequirements.add(MIN_UPPER_LETTERS_IN_PASSWORD); 391 requirementDescriptions.add(getResources().getQuantityString( 392 R.plurals.lockpassword_password_requires_uppercase, mPasswordMinUpperCase, 393 mPasswordMinUpperCase)); 394 } 395 if (mPasswordMinLowerCase > 0) { 396 passwordRequirements.add(MIN_LOWER_LETTERS_IN_PASSWORD); 397 requirementDescriptions.add(getResources().getQuantityString( 398 R.plurals.lockpassword_password_requires_lowercase, mPasswordMinLowerCase, 399 mPasswordMinLowerCase)); 400 } 401 if (mPasswordMinLetters > 0) { 402 if (mPasswordMinLetters > mPasswordMinUpperCase + mPasswordMinLowerCase) { 403 passwordRequirements.add(MIN_LETTER_IN_PASSWORD); 404 requirementDescriptions.add(getResources().getQuantityString( 405 R.plurals.lockpassword_password_requires_letters, mPasswordMinLetters, 406 mPasswordMinLetters)); 407 } 408 } 409 if (mPasswordMinNumeric > 0) { 410 passwordRequirements.add(MIN_NUMBER_IN_PASSWORD); 411 requirementDescriptions.add(getResources().getQuantityString( 412 R.plurals.lockpassword_password_requires_numeric, mPasswordMinNumeric, 413 mPasswordMinNumeric)); 414 } 415 if (mPasswordMinSymbols > 0) { 416 passwordRequirements.add(MIN_SYMBOLS_IN_PASSWORD); 417 requirementDescriptions.add(getResources().getQuantityString( 418 R.plurals.lockpassword_password_requires_symbols, mPasswordMinSymbols, 419 mPasswordMinSymbols)); 420 } 421 if (mPasswordMinNonLetter > 0) { 422 if (mPasswordMinNonLetter > mPasswordMinNumeric + mPasswordMinSymbols) { 423 passwordRequirements.add(MIN_NON_LETTER_IN_PASSWORD); 424 requirementDescriptions.add(getResources().getQuantityString( 425 R.plurals.lockpassword_password_requires_nonletter, mPasswordMinNonLetter, 426 427 mPasswordMinNonLetter)); 428 } 429 } 430 // Convert list to array. 431 mPasswordRequirements = passwordRequirements.stream().mapToInt(i -> i).toArray(); 432 mPasswordRestrictionView = 433 (RecyclerView) view.findViewById(R.id.password_requirements_view); 434 mPasswordRestrictionView.setLayoutManager(new LinearLayoutManager(getActivity())); 435 mPasswordRequirementAdapter = new PasswordRequirementAdapter(); 436 mPasswordRestrictionView.setAdapter(mPasswordRequirementAdapter); 437 } 438 439 @Override 440 protected int getMetricsCategory() { 441 return MetricsEvent.CHOOSE_LOCK_PASSWORD; 442 } 443 444 @Override 445 public void onResume() { 446 super.onResume(); 447 updateStage(mUiStage); 448 if (mSaveAndFinishWorker != null) { 449 mSaveAndFinishWorker.setListener(this); 450 } else { 451 mPasswordEntry.requestFocus(); 452 } 453 } 454 455 @Override 456 public void onPause() { 457 if (mSaveAndFinishWorker != null) { 458 mSaveAndFinishWorker.setListener(null); 459 } 460 super.onPause(); 461 } 462 463 @Override 464 public void onSaveInstanceState(Bundle outState) { 465 super.onSaveInstanceState(outState); 466 outState.putString(KEY_UI_STAGE, mUiStage.name()); 467 outState.putString(KEY_FIRST_PIN, mFirstPin); 468 outState.putString(KEY_CURRENT_PASSWORD, mCurrentPassword); 469 } 470 471 @Override 472 public void onActivityResult(int requestCode, int resultCode, 473 Intent data) { 474 super.onActivityResult(requestCode, resultCode, data); 475 switch (requestCode) { 476 case CONFIRM_EXISTING_REQUEST: 477 if (resultCode != Activity.RESULT_OK) { 478 getActivity().setResult(RESULT_FINISHED); 479 getActivity().finish(); 480 } else { 481 mCurrentPassword = data.getStringExtra( 482 ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); 483 } 484 break; 485 } 486 } 487 488 protected Intent getRedactionInterstitialIntent(Context context) { 489 return RedactionInterstitial.createStartIntent(context, mUserId); 490 } 491 492 protected void updateStage(Stage stage) { 493 final Stage previousStage = mUiStage; 494 mUiStage = stage; 495 updateUi(); 496 497 // If the stage changed, announce the header for accessibility. This 498 // is a no-op when accessibility is disabled. 499 if (previousStage != stage) { 500 mHeaderText.announceForAccessibility(mHeaderText.getText()); 501 } 502 } 503 504 /** 505 * Read the requirements from {@link DevicePolicyManager} and intent and aggregate them. 506 * 507 * @param intent the incoming intent 508 */ 509 private void processPasswordRequirements(Intent intent) { 510 final int dpmPasswordQuality = mLockPatternUtils.getRequestedPasswordQuality(mUserId); 511 mRequestedQuality = Math.max(intent.getIntExtra(LockPatternUtils.PASSWORD_TYPE_KEY, 512 mRequestedQuality), dpmPasswordQuality); 513 mPasswordMinLength = Math.max(Math.max( 514 LockPatternUtils.MIN_LOCK_PASSWORD_SIZE, 515 intent.getIntExtra(PASSWORD_MIN_KEY, mPasswordMinLength)), 516 mLockPatternUtils.getRequestedMinimumPasswordLength(mUserId)); 517 mPasswordMaxLength = intent.getIntExtra(PASSWORD_MAX_KEY, mPasswordMaxLength); 518 mPasswordMinLetters = Math.max(intent.getIntExtra(PASSWORD_MIN_LETTERS_KEY, 519 mPasswordMinLetters), mLockPatternUtils.getRequestedPasswordMinimumLetters( 520 mUserId)); 521 mPasswordMinUpperCase = Math.max(intent.getIntExtra(PASSWORD_MIN_UPPERCASE_KEY, 522 mPasswordMinUpperCase), mLockPatternUtils.getRequestedPasswordMinimumUpperCase( 523 mUserId)); 524 mPasswordMinLowerCase = Math.max(intent.getIntExtra(PASSWORD_MIN_LOWERCASE_KEY, 525 mPasswordMinLowerCase), mLockPatternUtils.getRequestedPasswordMinimumLowerCase( 526 mUserId)); 527 mPasswordMinNumeric = Math.max(intent.getIntExtra(PASSWORD_MIN_NUMERIC_KEY, 528 mPasswordMinNumeric), mLockPatternUtils.getRequestedPasswordMinimumNumeric( 529 mUserId)); 530 mPasswordMinSymbols = Math.max(intent.getIntExtra(PASSWORD_MIN_SYMBOLS_KEY, 531 mPasswordMinSymbols), mLockPatternUtils.getRequestedPasswordMinimumSymbols( 532 mUserId)); 533 mPasswordMinNonLetter = Math.max(intent.getIntExtra(PASSWORD_MIN_NONLETTER_KEY, 534 mPasswordMinNonLetter), mLockPatternUtils.getRequestedPasswordMinimumNonLetter( 535 mUserId)); 536 537 // Modify the value based on dpm policy. 538 switch (dpmPasswordQuality) { 539 case PASSWORD_QUALITY_ALPHABETIC: 540 if (mPasswordMinLetters == 0) { 541 mPasswordMinLetters = 1; 542 } 543 break; 544 case PASSWORD_QUALITY_ALPHANUMERIC: 545 if (mPasswordMinLetters == 0) { 546 mPasswordMinLetters = 1; 547 } 548 if (mPasswordMinNumeric == 0) { 549 mPasswordMinNumeric = 1; 550 } 551 break; 552 case PASSWORD_QUALITY_COMPLEX: 553 // Reserve all the requirements. 554 break; 555 default: 556 mPasswordMinNumeric = 0; 557 mPasswordMinLetters = 0; 558 mPasswordMinUpperCase = 0; 559 mPasswordMinLowerCase = 0; 560 mPasswordMinSymbols = 0; 561 mPasswordMinNonLetter = 0; 562 } 563 mPasswordMinLengthToFulfillAllPolicies = getMinLengthToFulfillAllPolicies(); 564 } 565 566 /** 567 * Validates PIN and returns the validation result. 568 * 569 * @param password the raw password the user typed in 570 * @return the validation result. 571 */ 572 private int validatePassword(String password) { 573 int errorCode = NO_ERROR; 574 575 if (password.length() < mPasswordMinLength) { 576 if (mPasswordMinLength > mPasswordMinLengthToFulfillAllPolicies) { 577 errorCode |= TOO_SHORT; 578 } 579 } else if (password.length() > mPasswordMaxLength) { 580 errorCode |= TOO_LONG; 581 } else { 582 // The length requirements are fulfilled. 583 if (mRequestedQuality == PASSWORD_QUALITY_NUMERIC_COMPLEX) { 584 // Check for repeated characters or sequences (e.g. '1234', '0000', '2468') 585 final int sequence = LockPatternUtils.maxLengthSequence(password); 586 if (sequence > LockPatternUtils.MAX_ALLOWED_SEQUENCE) { 587 errorCode |= CONTAIN_SEQUENTIAL_DIGITS; 588 } 589 } 590 // Is the password recently used? 591 if (mLockPatternUtils.checkPasswordHistory(password, mUserId)) { 592 errorCode |= RECENTLY_USED; 593 } 594 } 595 596 // Count different types of character. 597 int letters = 0; 598 int numbers = 0; 599 int lowercase = 0; 600 int symbols = 0; 601 int uppercase = 0; 602 int nonletter = 0; 603 for (int i = 0; i < password.length(); i++) { 604 char c = password.charAt(i); 605 // allow non control Latin-1 characters only 606 if (c < 32 || c > 127) { 607 errorCode |= CONTAIN_INVALID_CHARACTERS; 608 continue; 609 } 610 if (c >= '0' && c <= '9') { 611 numbers++; 612 nonletter++; 613 } else if (c >= 'A' && c <= 'Z') { 614 letters++; 615 uppercase++; 616 } else if (c >= 'a' && c <= 'z') { 617 letters++; 618 lowercase++; 619 } else { 620 symbols++; 621 nonletter++; 622 } 623 } 624 625 // Ensure no non-digits if we are requesting numbers. This shouldn't be possible unless 626 // user finds some way to bring up soft keyboard. 627 if (mRequestedQuality == PASSWORD_QUALITY_NUMERIC 628 || mRequestedQuality == PASSWORD_QUALITY_NUMERIC_COMPLEX) { 629 if (letters > 0 || symbols > 0) { 630 errorCode |= CONTAIN_NON_DIGITS; 631 } 632 } 633 634 // Check the requirements one by one. 635 for (int i = 0; i < mPasswordRequirements.length; i++) { 636 int passwordRestriction = mPasswordRequirements[i]; 637 switch (passwordRestriction) { 638 case MIN_LETTER_IN_PASSWORD: 639 if (letters < mPasswordMinLetters) { 640 errorCode |= NOT_ENOUGH_LETTER; 641 } 642 break; 643 case MIN_UPPER_LETTERS_IN_PASSWORD: 644 if (uppercase < mPasswordMinUpperCase) { 645 errorCode |= NOT_ENOUGH_UPPER_CASE; 646 } 647 break; 648 case MIN_LOWER_LETTERS_IN_PASSWORD: 649 if (lowercase < mPasswordMinLowerCase) { 650 errorCode |= NOT_ENOUGH_LOWER_CASE; 651 } 652 break; 653 case MIN_SYMBOLS_IN_PASSWORD: 654 if (symbols < mPasswordMinSymbols) { 655 errorCode |= NOT_ENOUGH_SYMBOLS; 656 } 657 break; 658 case MIN_NUMBER_IN_PASSWORD: 659 if (numbers < mPasswordMinNumeric) { 660 errorCode |= NOT_ENOUGH_DIGITS; 661 } 662 break; 663 case MIN_NON_LETTER_IN_PASSWORD: 664 if (nonletter < mPasswordMinNonLetter) { 665 errorCode |= NOT_ENOUGH_NON_LETTER; 666 } 667 break; 668 } 669 } 670 return errorCode; 671 } 672 673 public void handleNext() { 674 if (mSaveAndFinishWorker != null) return; 675 mChosenPassword = mPasswordEntry.getText().toString(); 676 if (TextUtils.isEmpty(mChosenPassword)) { 677 return; 678 } 679 if (mUiStage == Stage.Introduction) { 680 if (validatePassword(mChosenPassword) == NO_ERROR) { 681 mFirstPin = mChosenPassword; 682 mPasswordEntry.setText(""); 683 updateStage(Stage.NeedToConfirm); 684 } 685 } else if (mUiStage == Stage.NeedToConfirm) { 686 if (mFirstPin.equals(mChosenPassword)) { 687 startSaveAndFinish(); 688 } else { 689 CharSequence tmp = mPasswordEntry.getText(); 690 if (tmp != null) { 691 Selection.setSelection((Spannable) tmp, 0, tmp.length()); 692 } 693 updateStage(Stage.ConfirmWrong); 694 } 695 } 696 } 697 698 protected void setNextEnabled(boolean enabled) { 699 mNextButton.setEnabled(enabled); 700 } 701 702 protected void setNextText(int text) { 703 mNextButton.setText(text); 704 } 705 706 public void onClick(View v) { 707 switch (v.getId()) { 708 case R.id.next_button: 709 handleNext(); 710 break; 711 712 case R.id.cancel_button: 713 getActivity().finish(); 714 break; 715 } 716 } 717 718 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 719 // Check if this was the result of hitting the enter or "done" key 720 if (actionId == EditorInfo.IME_NULL 721 || actionId == EditorInfo.IME_ACTION_DONE 722 || actionId == EditorInfo.IME_ACTION_NEXT) { 723 handleNext(); 724 return true; 725 } 726 return false; 727 } 728 729 /** 730 * @param errorCode error code returned from {@link #validatePassword(String)}. 731 * @return an array of messages describing the error, important messages come first. 732 */ 733 private String[] convertErrorCodeToMessages(int errorCode) { 734 List<String> messages = new ArrayList<>(); 735 if ((errorCode & CONTAIN_INVALID_CHARACTERS) > 0) { 736 messages.add(getString(R.string.lockpassword_illegal_character)); 737 } 738 if ((errorCode & CONTAIN_NON_DIGITS) > 0) { 739 messages.add(getString(R.string.lockpassword_pin_contains_non_digits)); 740 } 741 if ((errorCode & NOT_ENOUGH_UPPER_CASE) > 0) { 742 messages.add(getResources().getQuantityString( 743 R.plurals.lockpassword_password_requires_uppercase, mPasswordMinUpperCase, 744 mPasswordMinUpperCase)); 745 } 746 if ((errorCode & NOT_ENOUGH_LOWER_CASE) > 0) { 747 messages.add(getResources().getQuantityString( 748 R.plurals.lockpassword_password_requires_lowercase, mPasswordMinLowerCase, 749 mPasswordMinLowerCase)); 750 } 751 if ((errorCode & NOT_ENOUGH_LETTER) > 0) { 752 messages.add(getResources().getQuantityString( 753 R.plurals.lockpassword_password_requires_letters, mPasswordMinLetters, 754 mPasswordMinLetters)); 755 } 756 if ((errorCode & NOT_ENOUGH_DIGITS) > 0) { 757 messages.add(getResources().getQuantityString( 758 R.plurals.lockpassword_password_requires_numeric, mPasswordMinNumeric, 759 mPasswordMinNumeric)); 760 } 761 if ((errorCode & NOT_ENOUGH_SYMBOLS) > 0) { 762 messages.add(getResources().getQuantityString( 763 R.plurals.lockpassword_password_requires_symbols, mPasswordMinSymbols, 764 mPasswordMinSymbols)); 765 } 766 if ((errorCode & NOT_ENOUGH_NON_LETTER) > 0) { 767 messages.add(getResources().getQuantityString( 768 R.plurals.lockpassword_password_requires_nonletter, mPasswordMinNonLetter, 769 mPasswordMinNonLetter)); 770 } 771 if ((errorCode & TOO_SHORT) > 0) { 772 messages.add(getString(mIsAlphaMode ? 773 R.string.lockpassword_password_too_short 774 : R.string.lockpassword_pin_too_short, mPasswordMinLength)); 775 } 776 if ((errorCode & TOO_LONG) > 0) { 777 messages.add(getString(mIsAlphaMode ? 778 R.string.lockpassword_password_too_long 779 : R.string.lockpassword_pin_too_long, mPasswordMaxLength + 1)); 780 } 781 if ((errorCode & CONTAIN_SEQUENTIAL_DIGITS) > 0) { 782 messages.add(getString(R.string.lockpassword_pin_no_sequential_digits)); 783 } 784 if ((errorCode & RECENTLY_USED) > 0) { 785 messages.add(getString((mIsAlphaMode) ? R.string.lockpassword_password_recently_used 786 : R.string.lockpassword_pin_recently_used)); 787 } 788 return messages.toArray(new String[0]); 789 } 790 791 private int getMinLengthToFulfillAllPolicies() { 792 final int minLengthForLetters = Math.max(mPasswordMinLetters, 793 mPasswordMinUpperCase + mPasswordMinLowerCase); 794 final int minLengthForNonLetters = Math.max(mPasswordMinNonLetter, 795 mPasswordMinSymbols + mPasswordMinNumeric); 796 return minLengthForLetters + minLengthForNonLetters; 797 } 798 799 /** 800 * Update the hint based on current Stage and length of password entry 801 */ 802 private void updateUi() { 803 final boolean canInput = mSaveAndFinishWorker == null; 804 String password = mPasswordEntry.getText().toString(); 805 final int length = password.length(); 806 if (mUiStage == Stage.Introduction) { 807 mPasswordRestrictionView.setVisibility(View.VISIBLE); 808 final int errorCode = validatePassword(password); 809 String[] messages = convertErrorCodeToMessages(errorCode); 810 // Update the fulfillment of requirements. 811 mPasswordRequirementAdapter.setRequirements(messages); 812 // Enable/Disable the next button accordingly. 813 setNextEnabled(errorCode == NO_ERROR); 814 } else { 815 // Hide password requirement view when we are just asking user to confirm the pw. 816 mPasswordRestrictionView.setVisibility(View.GONE); 817 setHeaderText(getString( 818 mIsAlphaMode ? mUiStage.alphaHint : mUiStage.numericHint)); 819 setNextEnabled(canInput && length > 0); 820 } 821 setNextText(mUiStage.buttonText); 822 mPasswordEntryInputDisabler.setInputEnabled(canInput); 823 } 824 825 private void setHeaderText(String text) { 826 // Only set the text if it is different than the existing one to avoid announcing again. 827 if (!TextUtils.isEmpty(mHeaderText.getText()) 828 && mHeaderText.getText().toString().equals(text)) { 829 return; 830 } 831 mHeaderText.setText(text); 832 } 833 834 public void afterTextChanged(Editable s) { 835 // Changing the text while error displayed resets to NeedToConfirm state 836 if (mUiStage == Stage.ConfirmWrong) { 837 mUiStage = Stage.NeedToConfirm; 838 } 839 // Schedule the UI update. 840 mTextChangedHandler.notifyAfterTextChanged(); 841 } 842 843 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 844 845 } 846 847 public void onTextChanged(CharSequence s, int start, int before, int count) { 848 849 } 850 851 private void startSaveAndFinish() { 852 if (mSaveAndFinishWorker != null) { 853 Log.w(TAG, "startSaveAndFinish with an existing SaveAndFinishWorker."); 854 return; 855 } 856 857 mPasswordEntryInputDisabler.setInputEnabled(false); 858 setNextEnabled(false); 859 860 mSaveAndFinishWorker = new SaveAndFinishWorker(); 861 mSaveAndFinishWorker.setListener(this); 862 863 getFragmentManager().beginTransaction().add(mSaveAndFinishWorker, 864 FRAGMENT_TAG_SAVE_AND_FINISH).commit(); 865 getFragmentManager().executePendingTransactions(); 866 867 final boolean required = getActivity().getIntent().getBooleanExtra( 868 EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true); 869 mSaveAndFinishWorker.start(mLockPatternUtils, required, mHasChallenge, mChallenge, 870 mChosenPassword, mCurrentPassword, mRequestedQuality, mUserId); 871 } 872 873 @Override 874 public void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData) { 875 getActivity().setResult(RESULT_FINISHED, resultData); 876 877 if (!wasSecureBefore) { 878 Intent intent = getRedactionInterstitialIntent(getActivity()); 879 if (intent != null) { 880 intent.putExtra(EXTRA_HIDE_DRAWER, mHideDrawer); 881 startActivity(intent); 882 } 883 } 884 getActivity().finish(); 885 } 886 887 class TextChangedHandler extends Handler { 888 private static final int ON_TEXT_CHANGED = 1; 889 private static final int DELAY_IN_MILLISECOND = 100; 890 891 /** 892 * With the introduction of delay, we batch processing the text changed event to reduce 893 * unnecessary UI updates. 894 */ 895 private void notifyAfterTextChanged() { 896 removeMessages(ON_TEXT_CHANGED); 897 sendEmptyMessageDelayed(ON_TEXT_CHANGED, DELAY_IN_MILLISECOND); 898 } 899 900 @Override 901 public void handleMessage(Message msg) { 902 if (getActivity() == null) { 903 return; 904 } 905 if (msg.what == ON_TEXT_CHANGED) { 906 updateUi(); 907 } 908 } 909 } 910 } 911 912 private static class SaveAndFinishWorker extends SaveChosenLockWorkerBase { 913 914 private String mChosenPassword; 915 private String mCurrentPassword; 916 private int mRequestedQuality; 917 918 public void start(LockPatternUtils utils, boolean required, 919 boolean hasChallenge, long challenge, 920 String chosenPassword, String currentPassword, int requestedQuality, int userId) { 921 prepare(utils, required, hasChallenge, challenge, userId); 922 923 mChosenPassword = chosenPassword; 924 mCurrentPassword = currentPassword; 925 mRequestedQuality = requestedQuality; 926 mUserId = userId; 927 928 start(); 929 } 930 931 @Override 932 protected Intent saveAndVerifyInBackground() { 933 Intent result = null; 934 mUtils.saveLockPassword(mChosenPassword, mCurrentPassword, mRequestedQuality, 935 mUserId); 936 937 if (mHasChallenge) { 938 byte[] token; 939 try { 940 token = mUtils.verifyPassword(mChosenPassword, mChallenge, mUserId); 941 } catch (RequestThrottledException e) { 942 token = null; 943 } 944 945 if (token == null) { 946 Log.e(TAG, "critical: no token returned for known good password."); 947 } 948 949 result = new Intent(); 950 result.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); 951 } 952 953 return result; 954 } 955 } 956 } 957