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