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