1 /* 2 * Copyright (C) 2018 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.car.settings.security; 18 19 import android.app.admin.DevicePolicyManager; 20 import android.content.Context; 21 import android.os.Bundle; 22 import android.os.Handler; 23 import android.os.Message; 24 import android.os.UserHandle; 25 import android.text.Editable; 26 import android.text.Selection; 27 import android.text.Spannable; 28 import android.text.TextWatcher; 29 import android.view.View; 30 import android.view.inputmethod.EditorInfo; 31 import android.view.inputmethod.InputMethodManager; 32 import android.widget.EditText; 33 import android.widget.TextView; 34 35 import androidx.annotation.DrawableRes; 36 import androidx.annotation.LayoutRes; 37 import androidx.annotation.NonNull; 38 import androidx.annotation.StringRes; 39 import androidx.annotation.VisibleForTesting; 40 41 import com.android.car.settings.R; 42 import com.android.car.settings.common.BaseFragment; 43 import com.android.car.settings.common.Logger; 44 import com.android.car.ui.toolbar.MenuItem; 45 import com.android.car.ui.toolbar.ProgressBarController; 46 import com.android.internal.widget.LockscreenCredential; 47 import com.android.internal.widget.TextViewInputDisabler; 48 49 import java.util.Arrays; 50 import java.util.List; 51 import java.util.Objects; 52 53 /** 54 * Fragment for choosing a lock password/pin. 55 */ 56 public class ChooseLockPinPasswordFragment extends BaseFragment { 57 58 private static final String LOCK_OPTIONS_DIALOG_TAG = "lock_options_dialog_tag"; 59 private static final String FRAGMENT_TAG_SAVE_PASSWORD_WORKER = "save_password_worker"; 60 private static final String STATE_UI_STAGE = "state_ui_stage"; 61 private static final String STATE_FIRST_ENTRY = "state_first_entry"; 62 private static final Logger LOG = new Logger(ChooseLockPinPasswordFragment.class); 63 private static final String EXTRA_IS_PIN = "extra_is_pin"; 64 65 private Stage mUiStage = Stage.Introduction; 66 67 private int mUserId; 68 private int mErrorCode = PasswordHelper.NO_ERROR; 69 70 private boolean mIsPin; 71 private boolean mIsAlphaMode; 72 73 // Password currently in the input field 74 private LockscreenCredential mCurrentEntry; 75 // Existing password that user previously set 76 private LockscreenCredential mExistingCredential; 77 // Password must be entered twice. This is what user entered the first time. 78 private LockscreenCredential mFirstEntry; 79 80 private PinPadView mPinPad; 81 private TextView mHintMessage; 82 private MenuItem mSecondaryButton; 83 private MenuItem mPrimaryButton; 84 private EditText mPasswordField; 85 private ProgressBarController mProgressBar; 86 87 private TextChangedHandler mTextChangedHandler = new TextChangedHandler(); 88 private TextViewInputDisabler mPasswordEntryInputDisabler; 89 private SaveLockWorker mSaveLockWorker; 90 private PasswordHelper mPasswordHelper; 91 92 /** 93 * Factory method for creating fragment in password mode 94 */ newPasswordInstance()95 public static ChooseLockPinPasswordFragment newPasswordInstance() { 96 ChooseLockPinPasswordFragment passwordFragment = new ChooseLockPinPasswordFragment(); 97 Bundle bundle = new Bundle(); 98 bundle.putBoolean(EXTRA_IS_PIN, false); 99 passwordFragment.setArguments(bundle); 100 return passwordFragment; 101 } 102 103 /** 104 * Factory method for creating fragment in Pin mode 105 */ newPinInstance()106 public static ChooseLockPinPasswordFragment newPinInstance() { 107 ChooseLockPinPasswordFragment passwordFragment = new ChooseLockPinPasswordFragment(); 108 Bundle bundle = new Bundle(); 109 bundle.putBoolean(EXTRA_IS_PIN, true); 110 passwordFragment.setArguments(bundle); 111 return passwordFragment; 112 } 113 114 @Override getToolbarMenuItems()115 public List<MenuItem> getToolbarMenuItems() { 116 return Arrays.asList(mPrimaryButton, mSecondaryButton); 117 } 118 119 @Override 120 @LayoutRes getLayoutId()121 protected int getLayoutId() { 122 return mIsPin ? R.layout.choose_lock_pin : R.layout.choose_lock_password; 123 } 124 125 @Override 126 @StringRes getTitleId()127 protected int getTitleId() { 128 return mIsPin ? R.string.security_lock_pin : R.string.security_lock_password; 129 } 130 131 @Override onCreate(Bundle savedInstanceState)132 public void onCreate(Bundle savedInstanceState) { 133 super.onCreate(savedInstanceState); 134 mUserId = UserHandle.myUserId(); 135 136 Bundle args = getArguments(); 137 if (args != null) { 138 mIsPin = args.getBoolean(EXTRA_IS_PIN); 139 mExistingCredential = args.getParcelable(PasswordHelper.EXTRA_CURRENT_SCREEN_LOCK); 140 } 141 142 mPasswordHelper = new PasswordHelper(mIsPin); 143 144 int passwordQuality = mPasswordHelper.getPasswordQuality(); 145 mIsAlphaMode = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == passwordQuality 146 || DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == passwordQuality 147 || DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == passwordQuality; 148 149 if (savedInstanceState != null) { 150 mUiStage = Stage.values()[savedInstanceState.getInt(STATE_UI_STAGE)]; 151 mFirstEntry = savedInstanceState.getParcelable(STATE_FIRST_ENTRY); 152 } 153 154 mPrimaryButton = new MenuItem.Builder(getContext()) 155 .setOnClickListener(i -> handlePrimaryButtonClick()) 156 .build(); 157 mSecondaryButton = new MenuItem.Builder(getContext()) 158 .setOnClickListener(i -> handleSecondaryButtonClick()) 159 .build(); 160 } 161 162 @Override onViewCreated(View view, Bundle savedInstanceState)163 public void onViewCreated(View view, Bundle savedInstanceState) { 164 super.onViewCreated(view, savedInstanceState); 165 166 mPasswordField = view.findViewById(R.id.password_entry); 167 mPasswordField.setOnEditorActionListener((textView, actionId, keyEvent) -> { 168 // Check if this was the result of hitting the enter or "done" key 169 if (actionId == EditorInfo.IME_NULL 170 || actionId == EditorInfo.IME_ACTION_DONE 171 || actionId == EditorInfo.IME_ACTION_NEXT) { 172 handlePrimaryButtonClick(); 173 return true; 174 } 175 return false; 176 }); 177 178 mPasswordField.addTextChangedListener(new TextWatcher() { 179 @Override 180 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 181 } 182 183 @Override 184 public void onTextChanged(CharSequence s, int start, int before, int count) { 185 186 } 187 188 @Override 189 public void afterTextChanged(Editable s) { 190 // Changing the text while error displayed resets to a normal state 191 if (mUiStage == Stage.ConfirmWrong) { 192 mUiStage = Stage.NeedToConfirm; 193 } else if (mUiStage == Stage.PasswordInvalid) { 194 mUiStage = Stage.Introduction; 195 } 196 // Schedule the UI update. 197 mTextChangedHandler.notifyAfterTextChanged(); 198 } 199 }); 200 201 mPasswordEntryInputDisabler = new TextViewInputDisabler(mPasswordField); 202 203 mHintMessage = view.findViewById(R.id.hint_text); 204 205 if (mIsPin) { 206 initPinView(view); 207 } else { 208 mPasswordField.requestFocus(); 209 InputMethodManager imm = (InputMethodManager) 210 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 211 if (imm != null) { 212 imm.showSoftInput(mPasswordField, InputMethodManager.SHOW_IMPLICIT); 213 } 214 } 215 216 // Re-attach to the exiting worker if there is one. 217 if (savedInstanceState != null) { 218 mSaveLockWorker = (SaveLockWorker) getFragmentManager().findFragmentByTag( 219 FRAGMENT_TAG_SAVE_PASSWORD_WORKER); 220 } 221 } 222 223 @Override onActivityCreated(Bundle savedInstanceState)224 public void onActivityCreated(Bundle savedInstanceState) { 225 super.onActivityCreated(savedInstanceState); 226 mProgressBar = getToolbar().getProgressBar(); 227 } 228 229 @Override onStart()230 public void onStart() { 231 super.onStart(); 232 updateStage(mUiStage); 233 234 if (mSaveLockWorker != null) { 235 mSaveLockWorker.setListener(this::onChosenLockSaveFinished); 236 } 237 } 238 239 @Override onSaveInstanceState(Bundle outState)240 public void onSaveInstanceState(Bundle outState) { 241 super.onSaveInstanceState(outState); 242 outState.putInt(STATE_UI_STAGE, mUiStage.ordinal()); 243 outState.putParcelable(STATE_FIRST_ENTRY, mFirstEntry); 244 } 245 246 @Override onStop()247 public void onStop() { 248 super.onStop(); 249 if (mSaveLockWorker != null) { 250 mSaveLockWorker.setListener(null); 251 } 252 mProgressBar.setVisible(false); 253 } 254 255 /** 256 * Append the argument to the end of the password entry field 257 */ appendToPasswordEntry(String text)258 private void appendToPasswordEntry(String text) { 259 mPasswordField.append(text); 260 } 261 262 /** 263 * Returns the string in the password entry field 264 */ 265 @NonNull getEnteredPassword()266 private LockscreenCredential getEnteredPassword() { 267 if (mIsPin) { 268 return LockscreenCredential.createPinOrNone(mPasswordField.getText()); 269 } else { 270 return LockscreenCredential.createPasswordOrNone(mPasswordField.getText()); 271 } 272 } 273 initPinView(View view)274 private void initPinView(View view) { 275 mPinPad = view.findViewById(R.id.pin_pad); 276 277 PinPadView.PinPadClickListener pinPadClickListener = new PinPadView.PinPadClickListener() { 278 @Override 279 public void onDigitKeyClick(String digit) { 280 appendToPasswordEntry(digit); 281 } 282 283 @Override 284 public void onBackspaceClick() { 285 LockscreenCredential pin = getEnteredPassword(); 286 if (pin.size() > 0) { 287 mPasswordField.getText().delete(mPasswordField.getSelectionEnd() - 1, 288 mPasswordField.getSelectionEnd()); 289 } 290 pin.zeroize(); 291 } 292 293 @Override 294 public void onEnterKeyClick() { 295 handlePrimaryButtonClick(); 296 } 297 }; 298 299 mPinPad.setPinPadClickListener(pinPadClickListener); 300 } 301 shouldEnableSubmit()302 private boolean shouldEnableSubmit() { 303 return getEnteredPassword().size() >= PasswordHelper.MIN_LENGTH 304 && (mSaveLockWorker == null || mSaveLockWorker.isFinished()); 305 } 306 updateSubmitButtonsState()307 private void updateSubmitButtonsState() { 308 boolean enabled = shouldEnableSubmit(); 309 310 mPrimaryButton.setEnabled(enabled); 311 if (mIsPin) { 312 mPinPad.setEnterKeyEnabled(enabled); 313 } 314 } 315 setPrimaryButtonText(@tringRes int textId)316 private void setPrimaryButtonText(@StringRes int textId) { 317 mPrimaryButton.setTitle(textId); 318 } 319 setSecondaryButtonEnabled(boolean enabled)320 private void setSecondaryButtonEnabled(boolean enabled) { 321 mSecondaryButton.setEnabled(enabled); 322 } 323 setSecondaryButtonText(@tringRes int textId)324 private void setSecondaryButtonText(@StringRes int textId) { 325 mSecondaryButton.setTitle(textId); 326 } 327 328 // Updates display message and proceed to next step according to the different text on 329 // the primary button. handlePrimaryButtonClick()330 private void handlePrimaryButtonClick() { 331 // Need to check this because it can be fired from the keyboard. 332 if (!shouldEnableSubmit()) { 333 return; 334 } 335 336 mCurrentEntry = getEnteredPassword(); 337 338 switch (mUiStage) { 339 case Introduction: 340 mErrorCode = mPasswordHelper.validate(mCurrentEntry); 341 if (mErrorCode == PasswordHelper.NO_ERROR) { 342 mFirstEntry = mCurrentEntry; 343 mPasswordField.setText(""); 344 updateStage(Stage.NeedToConfirm); 345 } else { 346 updateStage(Stage.PasswordInvalid); 347 mCurrentEntry.zeroize(); 348 } 349 break; 350 case NeedToConfirm: 351 case SaveFailure: 352 // Password must be entered twice. mFirstEntry is the one the user entered 353 // the first time. mCurrentEntry is what's currently in the input field 354 if (Objects.equals(mFirstEntry, mCurrentEntry)) { 355 startSaveAndFinish(); 356 } else { 357 CharSequence tmp = mPasswordField.getText(); 358 if (tmp != null) { 359 Selection.setSelection((Spannable) tmp, 0, tmp.length()); 360 } 361 updateStage(Stage.ConfirmWrong); 362 mCurrentEntry.zeroize(); 363 } 364 break; 365 default: 366 // Do nothing. 367 } 368 } 369 370 // Updates display message and proceed to next step according to the different text on 371 // the secondary button. handleSecondaryButtonClick()372 private void handleSecondaryButtonClick() { 373 if (mSaveLockWorker != null) { 374 return; 375 } 376 377 if (mUiStage.secondaryButtonText == R.string.lockpassword_clear_label) { 378 mPasswordField.setText(""); 379 mUiStage = Stage.Introduction; 380 setSecondaryButtonText(mUiStage.secondaryButtonText); 381 } else { 382 getFragmentHost().goBack(); 383 } 384 } 385 386 @VisibleForTesting onChosenLockSaveFinished(boolean isSaveSuccessful)387 void onChosenLockSaveFinished(boolean isSaveSuccessful) { 388 mProgressBar.setVisible(false); 389 if (isSaveSuccessful) { 390 onComplete(); 391 } else { 392 updateStage(Stage.SaveFailure); 393 } 394 } 395 396 // Starts an async task to save the chosen password. startSaveAndFinish()397 private void startSaveAndFinish() { 398 if (mSaveLockWorker != null && !mSaveLockWorker.isFinished()) { 399 LOG.v("startSaveAndFinish with a running SaveAndFinishWorker."); 400 return; 401 } 402 403 mPasswordEntryInputDisabler.setInputEnabled(false); 404 405 if (mSaveLockWorker == null) { 406 mSaveLockWorker = new SaveLockWorker(); 407 mSaveLockWorker.setListener(this::onChosenLockSaveFinished); 408 409 getFragmentManager() 410 .beginTransaction() 411 .add(mSaveLockWorker, FRAGMENT_TAG_SAVE_PASSWORD_WORKER) 412 .commitNow(); 413 } 414 415 mSaveLockWorker.start(mUserId, mCurrentEntry, mExistingCredential); 416 417 mProgressBar.setVisible(true); 418 updateSubmitButtonsState(); 419 } 420 421 // Updates the hint message, error, button text and state updateUi()422 private void updateUi() { 423 updateSubmitButtonsState(); 424 425 boolean inputAllowed = mSaveLockWorker == null || mSaveLockWorker.isFinished(); 426 427 if (mUiStage != Stage.Introduction) { 428 setSecondaryButtonEnabled(inputAllowed); 429 } 430 431 if (mIsPin) { 432 mPinPad.setEnterKeyIcon(mUiStage.enterKeyIcon); 433 } 434 435 switch (mUiStage) { 436 case Introduction: 437 case NeedToConfirm: 438 mPasswordField.setError(null); 439 mHintMessage.setText(getString(mUiStage.getHint(mIsAlphaMode))); 440 break; 441 case PasswordInvalid: 442 List<String> messages = 443 mPasswordHelper.convertErrorCodeToMessages(getContext(), mErrorCode); 444 setError(String.join(" ", messages)); 445 break; 446 case ConfirmWrong: 447 case SaveFailure: 448 setError(getString(mUiStage.getHint(mIsAlphaMode))); 449 break; 450 default: 451 // Do nothing 452 } 453 454 setPrimaryButtonText(mUiStage.primaryButtonText); 455 setSecondaryButtonText(mUiStage.secondaryButtonText); 456 mPasswordEntryInputDisabler.setInputEnabled(inputAllowed); 457 } 458 459 /** 460 * To show error in password, it is set directly on TextInputEditText. PIN can't use 461 * TextInputEditText because PIN field is not focusable therefore error won't show. Instead 462 * the error is shown as a hint message. 463 */ setError(String message)464 private void setError(String message) { 465 mHintMessage.setText(message); 466 } 467 468 @VisibleForTesting updateStage(Stage stage)469 void updateStage(Stage stage) { 470 mUiStage = stage; 471 updateUi(); 472 } 473 474 @VisibleForTesting onComplete()475 void onComplete() { 476 if (mCurrentEntry != null) { 477 mCurrentEntry.zeroize(); 478 } 479 480 if (mExistingCredential != null) { 481 mExistingCredential.zeroize(); 482 } 483 484 if (mFirstEntry != null) { 485 mFirstEntry.zeroize(); 486 } 487 488 mPasswordField.setText(""); 489 490 getActivity().finish(); 491 } 492 493 // Keep track internally of where the user is in choosing a password. 494 @VisibleForTesting 495 enum Stage { 496 Introduction( 497 R.string.choose_lock_password_hints, 498 R.string.choose_lock_pin_hints, 499 R.string.continue_button_text, 500 R.string.lockpassword_cancel_label, 501 R.drawable.ic_arrow_forward), 502 503 PasswordInvalid( 504 R.string.lockpassword_invalid_password, 505 R.string.lockpin_invalid_pin, 506 R.string.continue_button_text, 507 R.string.lockpassword_clear_label, 508 R.drawable.ic_arrow_forward), 509 510 NeedToConfirm( 511 R.string.confirm_your_password_header, 512 R.string.confirm_your_pin_header, 513 R.string.lockpassword_confirm_label, 514 R.string.lockpassword_cancel_label, 515 R.drawable.ic_check), 516 517 ConfirmWrong( 518 R.string.confirm_passwords_dont_match, 519 R.string.confirm_pins_dont_match, 520 R.string.continue_button_text, 521 R.string.lockpassword_cancel_label, 522 R.drawable.ic_check), 523 524 SaveFailure( 525 R.string.error_saving_password, 526 R.string.error_saving_lockpin, 527 R.string.lockscreen_retry_button_text, 528 R.string.lockpassword_cancel_label, 529 R.drawable.ic_check); 530 531 public final int alphaHint; 532 public final int numericHint; 533 public final int primaryButtonText; 534 public final int secondaryButtonText; 535 public final int enterKeyIcon; 536 Stage(@tringRes int hintInAlpha, @StringRes int hintInNumeric, @StringRes int primaryButtonText, @StringRes int secondaryButtonText, @DrawableRes int enterKeyIcon)537 Stage(@StringRes int hintInAlpha, 538 @StringRes int hintInNumeric, 539 @StringRes int primaryButtonText, 540 @StringRes int secondaryButtonText, 541 @DrawableRes int enterKeyIcon) { 542 this.alphaHint = hintInAlpha; 543 this.numericHint = hintInNumeric; 544 this.primaryButtonText = primaryButtonText; 545 this.secondaryButtonText = secondaryButtonText; 546 this.enterKeyIcon = enterKeyIcon; 547 } 548 549 @StringRes getHint(boolean isAlpha)550 public int getHint(boolean isAlpha) { 551 if (isAlpha) { 552 return alphaHint; 553 } else { 554 return numericHint; 555 } 556 } 557 } 558 559 /** 560 * Handler that batches text changed events 561 */ 562 private class TextChangedHandler extends Handler { 563 private static final int ON_TEXT_CHANGED = 1; 564 private static final int DELAY_IN_MILLISECOND = 100; 565 566 /** 567 * With the introduction of delay, we batch processing the text changed event to reduce 568 * unnecessary UI updates. 569 */ notifyAfterTextChanged()570 private void notifyAfterTextChanged() { 571 removeMessages(ON_TEXT_CHANGED); 572 sendEmptyMessageDelayed(ON_TEXT_CHANGED, DELAY_IN_MILLISECOND); 573 } 574 575 @Override handleMessage(Message msg)576 public void handleMessage(Message msg) { 577 if (msg.what == ON_TEXT_CHANGED) { 578 mErrorCode = PasswordHelper.NO_ERROR; 579 updateUi(); 580 } 581 } 582 } 583 } 584