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