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