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