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