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.Activity; 20 import android.app.admin.DevicePolicyManager; 21 import android.content.Context; 22 import android.os.Bundle; 23 import android.os.Handler; 24 import android.os.Message; 25 import android.os.UserHandle; 26 import android.text.Editable; 27 import android.text.Selection; 28 import android.text.Spannable; 29 import android.text.TextWatcher; 30 import android.view.View; 31 import android.view.inputmethod.EditorInfo; 32 import android.view.inputmethod.InputMethodManager; 33 import android.widget.EditText; 34 import android.widget.TextView; 35 36 import androidx.annotation.DrawableRes; 37 import androidx.annotation.LayoutRes; 38 import androidx.annotation.NonNull; 39 import androidx.annotation.StringRes; 40 import androidx.annotation.VisibleForTesting; 41 42 import com.android.car.settings.R; 43 import com.android.car.settings.common.BaseFragment; 44 import com.android.car.settings.common.Logger; 45 import com.android.car.ui.toolbar.MenuItem; 46 import com.android.car.ui.toolbar.ProgressBarController; 47 import com.android.internal.widget.LockscreenCredential; 48 import com.android.internal.widget.TextViewInputDisabler; 49 50 import java.util.Arrays; 51 import java.util.List; 52 import java.util.Objects; 53 54 /** 55 * Fragment for choosing a lock password/pin. 56 */ 57 public class ChooseLockPinPasswordFragment extends BaseFragment { 58 59 private static final String LOCK_OPTIONS_DIALOG_TAG = "lock_options_dialog_tag"; 60 private static final String FRAGMENT_TAG_SAVE_PASSWORD_WORKER = "save_password_worker"; 61 private static final String STATE_UI_STAGE = "state_ui_stage"; 62 private static final String STATE_FIRST_ENTRY = "state_first_entry"; 63 private static final Logger LOG = new Logger(ChooseLockPinPasswordFragment.class); 64 private static final String EXTRA_IS_PIN = "extra_is_pin"; 65 66 private Stage mUiStage = Stage.Introduction; 67 68 private int mUserId; 69 private int mErrorCode = PasswordHelper.NO_ERROR; 70 71 private boolean mIsPin; 72 private boolean mIsAlphaMode; 73 74 // Password currently in the input field 75 private LockscreenCredential mCurrentEntry; 76 // Existing password that user previously set 77 private LockscreenCredential mExistingCredential; 78 // Password must be entered twice. This is what user entered the first time. 79 private LockscreenCredential mFirstEntry; 80 81 private PinPadView mPinPad; 82 private TextView mHintMessage; 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); 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 if (mExistingCredential != null) { 141 mExistingCredential = mExistingCredential.duplicate(); 142 } 143 } 144 145 mPasswordHelper = new PasswordHelper(mIsPin); 146 147 int passwordQuality = mPasswordHelper.getPasswordQuality(); 148 mIsAlphaMode = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == passwordQuality 149 || DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == passwordQuality 150 || DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == passwordQuality; 151 152 if (savedInstanceState != null) { 153 mUiStage = Stage.values()[savedInstanceState.getInt(STATE_UI_STAGE)]; 154 mFirstEntry = savedInstanceState.getParcelable(STATE_FIRST_ENTRY); 155 } 156 157 mPrimaryButton = new MenuItem.Builder(getContext()) 158 .setOnClickListener(i -> handlePrimaryButtonClick()) 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 if (isResumed()) { 198 mTextChangedHandler.notifyAfterTextChanged(); 199 } 200 } 201 }); 202 203 mPasswordEntryInputDisabler = new TextViewInputDisabler(mPasswordField); 204 205 mHintMessage = view.findViewById(R.id.hint_text); 206 207 if (mIsPin) { 208 initPinView(view); 209 } else { 210 mPasswordField.requestFocus(); 211 InputMethodManager imm = (InputMethodManager) 212 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 213 if (imm != null) { 214 imm.showSoftInput(mPasswordField, InputMethodManager.SHOW_IMPLICIT); 215 } 216 } 217 218 // Re-attach to the exiting worker if there is one. 219 if (savedInstanceState != null) { 220 mSaveLockWorker = (SaveLockWorker) getFragmentManager().findFragmentByTag( 221 FRAGMENT_TAG_SAVE_PASSWORD_WORKER); 222 } 223 } 224 225 @Override onActivityCreated(Bundle savedInstanceState)226 public void onActivityCreated(Bundle savedInstanceState) { 227 super.onActivityCreated(savedInstanceState); 228 mProgressBar = getToolbar().getProgressBar(); 229 } 230 231 @Override onStart()232 public void onStart() { 233 super.onStart(); 234 updateStage(mUiStage); 235 236 if (mSaveLockWorker != null) { 237 mSaveLockWorker.setListener(this::onChosenLockSaveFinished); 238 } 239 } 240 241 @Override onSaveInstanceState(Bundle outState)242 public void onSaveInstanceState(Bundle outState) { 243 super.onSaveInstanceState(outState); 244 outState.putInt(STATE_UI_STAGE, mUiStage.ordinal()); 245 outState.putParcelable(STATE_FIRST_ENTRY, mFirstEntry); 246 } 247 248 @Override onStop()249 public void onStop() { 250 super.onStop(); 251 if (mSaveLockWorker != null) { 252 mSaveLockWorker.setListener(null); 253 } 254 mProgressBar.setVisible(false); 255 } 256 257 @Override onDestroy()258 public void onDestroy() { 259 super.onDestroy(); 260 mPasswordField.setText(null); 261 262 PasswordHelper.zeroizeCredentials(mCurrentEntry, mExistingCredential, mFirstEntry); 263 } 264 265 /** 266 * Append the argument to the end of the password entry field 267 */ appendToPasswordEntry(String text)268 private void appendToPasswordEntry(String text) { 269 mPasswordField.append(text); 270 } 271 272 /** 273 * Returns the string in the password entry field 274 */ 275 @NonNull getEnteredPassword()276 private LockscreenCredential getEnteredPassword() { 277 if (mIsPin) { 278 return LockscreenCredential.createPinOrNone(mPasswordField.getText()); 279 } else { 280 return LockscreenCredential.createPasswordOrNone(mPasswordField.getText()); 281 } 282 } 283 initPinView(View view)284 private void initPinView(View view) { 285 mPinPad = view.findViewById(R.id.pin_pad); 286 287 PinPadView.PinPadClickListener pinPadClickListener = new PinPadView.PinPadClickListener() { 288 @Override 289 public void onDigitKeyClick(String digit) { 290 appendToPasswordEntry(digit); 291 } 292 293 @Override 294 public void onBackspaceClick() { 295 LockscreenCredential pin = getEnteredPassword(); 296 if (pin.size() > 0) { 297 mPasswordField.getText().delete(mPasswordField.getSelectionEnd() - 1, 298 mPasswordField.getSelectionEnd()); 299 } 300 pin.zeroize(); 301 } 302 303 @Override 304 public void onEnterKeyClick() { 305 handlePrimaryButtonClick(); 306 } 307 }; 308 309 mPinPad.setPinPadClickListener(pinPadClickListener); 310 } 311 shouldEnableSubmit()312 private boolean shouldEnableSubmit() { 313 return getEnteredPassword().size() >= PasswordHelper.MIN_LENGTH 314 && (mSaveLockWorker == null || mSaveLockWorker.isFinished()); 315 } 316 updateSubmitButtonsState()317 private void updateSubmitButtonsState() { 318 boolean enabled = shouldEnableSubmit(); 319 320 mPrimaryButton.setEnabled(enabled); 321 if (mIsPin) { 322 mPinPad.setEnterKeyEnabled(enabled); 323 } 324 } 325 setPrimaryButtonText(@tringRes int textId)326 private void setPrimaryButtonText(@StringRes int textId) { 327 mPrimaryButton.setTitle(textId); 328 } 329 330 // Updates display message and proceed to next step according to the different text on 331 // the primary button. handlePrimaryButtonClick()332 private void handlePrimaryButtonClick() { 333 // Need to check this because it can be fired from the keyboard. 334 if (!shouldEnableSubmit()) { 335 return; 336 } 337 338 mCurrentEntry = getEnteredPassword(); 339 340 switch (mUiStage) { 341 case Introduction: 342 mErrorCode = mPasswordHelper.validate(mCurrentEntry); 343 if (mErrorCode == PasswordHelper.NO_ERROR) { 344 mFirstEntry = mCurrentEntry; 345 mPasswordField.setText(""); 346 updateStage(Stage.NeedToConfirm); 347 } else { 348 updateStage(Stage.PasswordInvalid); 349 mCurrentEntry.zeroize(); 350 } 351 break; 352 case NeedToConfirm: 353 case SaveFailure: 354 // Password must be entered twice. mFirstEntry is the one the user entered 355 // the first time. mCurrentEntry is what's currently in the input field 356 if (Objects.equals(mFirstEntry, mCurrentEntry)) { 357 startSaveAndFinish(); 358 } else { 359 CharSequence tmp = mPasswordField.getText(); 360 if (tmp != null) { 361 Selection.setSelection((Spannable) tmp, 0, tmp.length()); 362 } 363 updateStage(Stage.ConfirmWrong); 364 mCurrentEntry.zeroize(); 365 } 366 break; 367 default: 368 // Do nothing. 369 } 370 } 371 372 @VisibleForTesting onChosenLockSaveFinished(boolean isSaveSuccessful)373 void onChosenLockSaveFinished(boolean isSaveSuccessful) { 374 mProgressBar.setVisible(false); 375 if (isSaveSuccessful) { 376 onComplete(); 377 } else { 378 updateStage(Stage.SaveFailure); 379 } 380 } 381 382 // Starts an async task to save the chosen password. startSaveAndFinish()383 private void startSaveAndFinish() { 384 if (mSaveLockWorker != null && !mSaveLockWorker.isFinished()) { 385 LOG.v("startSaveAndFinish with a running SaveAndFinishWorker."); 386 return; 387 } 388 389 mPasswordEntryInputDisabler.setInputEnabled(false); 390 391 if (mSaveLockWorker == null) { 392 mSaveLockWorker = new SaveLockWorker(); 393 mSaveLockWorker.setListener(this::onChosenLockSaveFinished); 394 395 getFragmentManager() 396 .beginTransaction() 397 .add(mSaveLockWorker, FRAGMENT_TAG_SAVE_PASSWORD_WORKER) 398 .commitNow(); 399 } 400 401 mSaveLockWorker.start(mUserId, mCurrentEntry, mExistingCredential); 402 403 mProgressBar.setVisible(true); 404 updateSubmitButtonsState(); 405 } 406 407 // Updates the hint message, error, button text and state updateUi()408 private void updateUi() { 409 updateSubmitButtonsState(); 410 411 boolean inputAllowed = mSaveLockWorker == null || mSaveLockWorker.isFinished(); 412 413 if (mIsPin) { 414 mPinPad.setEnterKeyIcon(mUiStage.enterKeyIcon); 415 } 416 417 switch (mUiStage) { 418 case Introduction: 419 case NeedToConfirm: 420 mPasswordField.setError(null); 421 mHintMessage.setText(getString(mUiStage.getHint(mIsAlphaMode))); 422 break; 423 case PasswordInvalid: 424 List<String> messages = 425 mPasswordHelper.convertErrorCodeToMessages(getContext(), mErrorCode); 426 setError(String.join(" ", messages)); 427 break; 428 case ConfirmWrong: 429 case SaveFailure: 430 setError(getString(mUiStage.getHint(mIsAlphaMode))); 431 break; 432 default: 433 // Do nothing 434 } 435 436 setPrimaryButtonText(mUiStage.primaryButtonText); 437 mPasswordEntryInputDisabler.setInputEnabled(inputAllowed); 438 } 439 440 /** 441 * To show error in password, it is set directly on TextInputEditText. PIN can't use 442 * TextInputEditText because PIN field is not focusable therefore error won't show. Instead 443 * the error is shown as a hint message. 444 */ setError(String message)445 private void setError(String message) { 446 mHintMessage.setText(message); 447 } 448 449 @VisibleForTesting updateStage(Stage stage)450 void updateStage(Stage stage) { 451 mUiStage = stage; 452 updateUi(); 453 } 454 455 @VisibleForTesting onComplete()456 void onComplete() { 457 if (mCurrentEntry != null) { 458 mCurrentEntry.zeroize(); 459 } 460 461 if (mExistingCredential != null) { 462 mExistingCredential.zeroize(); 463 } 464 465 if (mFirstEntry != null) { 466 mFirstEntry.zeroize(); 467 } 468 469 mPasswordField.setText(""); 470 471 getActivity().setResult(Activity.RESULT_OK); 472 getActivity().finish(); 473 } 474 475 // Keep track internally of where the user is in choosing a password. 476 @VisibleForTesting 477 enum Stage { 478 Introduction( 479 R.string.choose_lock_password_hints, 480 R.string.choose_lock_pin_hints, 481 R.string.continue_button_text, 482 R.drawable.ic_arrow_forward), 483 484 PasswordInvalid( 485 R.string.lockpassword_invalid_password, 486 R.string.lockpin_invalid_pin, 487 R.string.continue_button_text, 488 R.drawable.ic_arrow_forward), 489 490 NeedToConfirm( 491 R.string.confirm_your_password_header, 492 R.string.confirm_your_pin_header, 493 R.string.lockpassword_confirm_label, 494 R.drawable.ic_check), 495 496 ConfirmWrong( 497 R.string.confirm_passwords_dont_match, 498 R.string.confirm_pins_dont_match, 499 R.string.lockpassword_confirm_label, 500 R.drawable.ic_check), 501 502 SaveFailure( 503 R.string.error_saving_password, 504 R.string.error_saving_lockpin, 505 R.string.lockscreen_retry_button_text, 506 R.drawable.ic_check); 507 508 public final int alphaHint; 509 public final int numericHint; 510 public final int primaryButtonText; 511 public final int enterKeyIcon; 512 Stage(@tringRes int hintInAlpha, @StringRes int hintInNumeric, @StringRes int primaryButtonText, @DrawableRes int enterKeyIcon)513 Stage(@StringRes int hintInAlpha, 514 @StringRes int hintInNumeric, 515 @StringRes int primaryButtonText, 516 @DrawableRes int enterKeyIcon) { 517 this.alphaHint = hintInAlpha; 518 this.numericHint = hintInNumeric; 519 this.primaryButtonText = primaryButtonText; 520 this.enterKeyIcon = enterKeyIcon; 521 } 522 523 @StringRes getHint(boolean isAlpha)524 public int getHint(boolean isAlpha) { 525 if (isAlpha) { 526 return alphaHint; 527 } else { 528 return numericHint; 529 } 530 } 531 } 532 533 /** 534 * Handler that batches text changed events 535 */ 536 private class TextChangedHandler extends Handler { 537 private static final int ON_TEXT_CHANGED = 1; 538 private static final int DELAY_IN_MILLISECOND = 100; 539 540 /** 541 * With the introduction of delay, we batch processing the text changed event to reduce 542 * unnecessary UI updates. 543 */ notifyAfterTextChanged()544 private void notifyAfterTextChanged() { 545 removeMessages(ON_TEXT_CHANGED); 546 sendEmptyMessageDelayed(ON_TEXT_CHANGED, DELAY_IN_MILLISECOND); 547 } 548 549 @Override handleMessage(Message msg)550 public void handleMessage(Message msg) { 551 if (msg.what == ON_TEXT_CHANGED) { 552 mErrorCode = PasswordHelper.NO_ERROR; 553 updateUi(); 554 } 555 } 556 } 557 } 558