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.os.Bundle;
21 import android.os.UserHandle;
22 import android.view.View;
23 import android.widget.TextView;
24 
25 import androidx.annotation.LayoutRes;
26 import androidx.annotation.StringRes;
27 import androidx.annotation.VisibleForTesting;
28 
29 import com.android.car.settings.R;
30 import com.android.car.settings.common.BaseFragment;
31 import com.android.car.settings.common.Logger;
32 import com.android.car.ui.toolbar.MenuItem;
33 import com.android.internal.widget.LockPatternUtils;
34 import com.android.internal.widget.LockPatternView;
35 import com.android.internal.widget.LockPatternView.Cell;
36 import com.android.internal.widget.LockPatternView.DisplayMode;
37 import com.android.internal.widget.LockscreenCredential;
38 
39 import com.google.android.collect.Lists;
40 
41 import java.util.Arrays;
42 import java.util.Collections;
43 import java.util.List;
44 
45 /**
46  * Fragment for choosing security lock pattern.
47  */
48 public class ChooseLockPatternFragment extends BaseFragment {
49 
50     private static final Logger LOG = new Logger(ChooseLockPatternFragment.class);
51     private static final String FRAGMENT_TAG_SAVE_PATTERN_WORKER = "save_pattern_worker";
52     private static final String STATE_UI_STAGE = "state_ui_stage";
53     private static final String STATE_CHOSEN_PATTERN = "state_chosen_pattern";
54     private static final int ID_EMPTY_MESSAGE = -1;
55     /**
56      * The patten used during the help screen to show how to draw a pattern.
57      */
58     private final List<LockPatternView.Cell> mAnimatePattern =
59             Collections.unmodifiableList(Lists.newArrayList(
60                     LockPatternView.Cell.of(0, 0),
61                     LockPatternView.Cell.of(0, 1),
62                     LockPatternView.Cell.of(1, 1),
63                     LockPatternView.Cell.of(2, 1)
64             ));
65     // How long we wait to clear a wrong pattern
66     private int mWrongPatternClearTimeOut;
67     private int mUserId;
68     private Stage mUiStage = Stage.Introduction;
69     private LockPatternView mLockPatternView;
70     private TextView mMessageText;
71     private LockscreenCredential mChosenPattern;
72     private MenuItem mSecondaryButton;
73     private MenuItem mPrimaryButton;
74     // Existing pattern that user previously set
75     private LockscreenCredential mCurrentCredential;
76     private SaveLockWorker mSaveLockWorker;
77     private Runnable mClearPatternRunnable = () -> mLockPatternView.clearPattern();
78     // The pattern listener that responds according to a user choosing a new
79     // lock pattern.
80     private final LockPatternView.OnPatternListener mChooseNewLockPatternListener =
81             new LockPatternView.OnPatternListener() {
82                 @Override
83                 public void onPatternStart() {
84                     mLockPatternView.removeCallbacks(mClearPatternRunnable);
85                     updateUIWhenPatternInProgress();
86                 }
87 
88                 @Override
89                 public void onPatternCleared() {
90                     mLockPatternView.removeCallbacks(mClearPatternRunnable);
91                 }
92 
93                 @Override
94                 public void onPatternDetected(List<LockPatternView.Cell> pattern) {
95                     switch (mUiStage) {
96                         case Introduction:
97                         case ChoiceTooShort:
98                             handlePatternEntered(pattern);
99                             break;
100                         case ConfirmWrong:
101                         case NeedToConfirm:
102                             handleConfirmPattern(pattern);
103                             break;
104                         default:
105                             throw new IllegalStateException("Unexpected stage " + mUiStage
106                                     + " when entering the pattern.");
107                     }
108                 }
109 
110                 @Override
111                 public void onPatternCellAdded(List<Cell> pattern) {
112                 }
113 
114                 private void handleConfirmPattern(List<LockPatternView.Cell> pattern) {
115                     if (mChosenPattern == null) {
116                         throw new IllegalStateException(
117                                 "null chosen pattern in stage 'need to confirm");
118                     }
119                     try (LockscreenCredential credential =
120                             LockscreenCredential.createPattern(pattern)) {
121                         if (mChosenPattern.equals(credential)) {
122                             updateStage(Stage.ChoiceConfirmed);
123                         } else {
124                             updateStage(Stage.ConfirmWrong);
125                         }
126                     }
127                 }
128 
129                 private void handlePatternEntered(List<LockPatternView.Cell> pattern) {
130                     if (pattern.size() < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) {
131                         updateStage(Stage.ChoiceTooShort);
132                     } else {
133                         mChosenPattern = LockscreenCredential.createPattern(pattern);
134                         updateStage(Stage.FirstChoiceValid);
135                     }
136                 }
137             };
138 
139     /**
140      * Factory method for creating ChooseLockPatternFragment
141      */
newInstance()142     public static ChooseLockPatternFragment newInstance() {
143         ChooseLockPatternFragment patternFragment = new ChooseLockPatternFragment();
144         return patternFragment;
145     }
146 
147     @Override
getToolbarMenuItems()148     public List<MenuItem> getToolbarMenuItems() {
149         return Arrays.asList(mSecondaryButton, mPrimaryButton);
150     }
151 
152     @Override
153     @LayoutRes
getLayoutId()154     protected int getLayoutId() {
155         return R.layout.choose_lock_pattern;
156     }
157 
158     @Override
159     @StringRes
getTitleId()160     protected int getTitleId() {
161         return R.string.security_lock_pattern;
162     }
163 
164     @Override
onCreate(Bundle savedInstanceState)165     public void onCreate(Bundle savedInstanceState) {
166         super.onCreate(savedInstanceState);
167         mWrongPatternClearTimeOut = getResources().getInteger(R.integer.clear_content_timeout_ms);
168         mUserId = UserHandle.myUserId();
169 
170         Bundle args = getArguments();
171         if (args != null) {
172             mCurrentCredential = args.getParcelable(PasswordHelper.EXTRA_CURRENT_SCREEN_LOCK);
173             if (mCurrentCredential != null) {
174                 mCurrentCredential = mCurrentCredential.duplicate();
175             }
176         }
177 
178         if (savedInstanceState != null) {
179             mUiStage = Stage.values()[savedInstanceState.getInt(STATE_UI_STAGE)];
180             mChosenPattern = savedInstanceState.getParcelable(STATE_CHOSEN_PATTERN);
181         }
182 
183         mPrimaryButton = new MenuItem.Builder(getContext())
184                 .setOnClickListener(i -> handlePrimaryButtonClick())
185                 .build();
186         mSecondaryButton = new MenuItem.Builder(getContext())
187                 .setOnClickListener(i -> handleSecondaryButtonClick())
188                 .build();
189     }
190 
191     @Override
onViewCreated(View view, Bundle savedInstanceState)192     public void onViewCreated(View view, Bundle savedInstanceState) {
193         super.onViewCreated(view, savedInstanceState);
194 
195         mMessageText = view.findViewById(R.id.title_text);
196         mMessageText.setText(getString(R.string.choose_lock_pattern_message));
197 
198         mLockPatternView = view.findViewById(R.id.lockPattern);
199         mLockPatternView.setVisibility(View.VISIBLE);
200         mLockPatternView.setEnabled(true);
201         mLockPatternView.setFadePattern(false);
202         mLockPatternView.clearPattern();
203         mLockPatternView.setOnPatternListener(mChooseNewLockPatternListener);
204 
205         // Re-attach to the exiting worker if there is one.
206         if (savedInstanceState != null) {
207             mSaveLockWorker = (SaveLockWorker) getFragmentManager().findFragmentByTag(
208                     FRAGMENT_TAG_SAVE_PATTERN_WORKER);
209         }
210     }
211 
212     @Override
onStart()213     public void onStart() {
214         super.onStart();
215         updateStage(mUiStage);
216 
217         if (mSaveLockWorker != null) {
218             setPrimaryButtonEnabled(true);
219             mSaveLockWorker.setListener(this::onChosenLockSaveFinished);
220         }
221     }
222 
223     @Override
onSaveInstanceState(Bundle outState)224     public void onSaveInstanceState(Bundle outState) {
225         super.onSaveInstanceState(outState);
226         outState.putInt(STATE_UI_STAGE, mUiStage.ordinal());
227         outState.putParcelable(STATE_CHOSEN_PATTERN, mChosenPattern);
228     }
229 
230     @Override
onStop()231     public void onStop() {
232         super.onStop();
233         if (mSaveLockWorker != null) {
234             mSaveLockWorker.setListener(null);
235         }
236         getToolbar().getProgressBar().setVisible(false);
237     }
238 
239     @Override
onDestroy()240     public void onDestroy() {
241         super.onDestroy();
242 
243         mLockPatternView.clearPattern();
244 
245         PasswordHelper.zeroizeCredentials(mChosenPattern, mCurrentCredential);
246     }
247 
248     /**
249      * Updates the messages and buttons appropriate to what stage the user
250      * is at in choosing a pattern. This doesn't handle clearing out the pattern;
251      * the pattern is expected to be in the right state.
252      *
253      * @param stage The stage UI should be updated to match with.
254      */
updateStage(Stage stage)255     protected void updateStage(Stage stage) {
256         mUiStage = stage;
257 
258         // Message mText, visibility and
259         // mEnabled state all known from the stage
260         mMessageText.setText(stage.mMessageId);
261 
262         if (stage.mSecondaryButtonState == SecondaryButtonState.Gone) {
263             setSecondaryButtonVisible(false);
264         } else {
265             setSecondaryButtonVisible(true);
266             setSecondaryButtonText(stage.mSecondaryButtonState.mTextResId);
267             setSecondaryButtonEnabled(stage.mSecondaryButtonState.mEnabled);
268         }
269 
270         setPrimaryButtonText(stage.mPrimaryButtonState.mText);
271         setPrimaryButtonEnabled(stage.mPrimaryButtonState.mEnabled);
272 
273         // same for whether the pattern is mEnabled
274         if (stage.mPatternEnabled) {
275             mLockPatternView.enableInput();
276         } else {
277             mLockPatternView.disableInput();
278         }
279 
280         // the rest of the stuff varies enough that it is easier just to handle
281         // on a case by case basis.
282         mLockPatternView.setDisplayMode(DisplayMode.Correct);
283 
284         switch (mUiStage) {
285             case Introduction:
286                 mLockPatternView.clearPattern();
287                 break;
288             case HelpScreen:
289                 mLockPatternView.setPattern(DisplayMode.Animate, mAnimatePattern);
290                 break;
291             case ChoiceTooShort:
292                 mLockPatternView.setDisplayMode(DisplayMode.Wrong);
293                 postClearPatternRunnable();
294                 break;
295             case FirstChoiceValid:
296                 break;
297             case NeedToConfirm:
298                 mLockPatternView.clearPattern();
299                 break;
300             case ConfirmWrong:
301                 mLockPatternView.setDisplayMode(DisplayMode.Wrong);
302                 postClearPatternRunnable();
303                 break;
304             case ChoiceConfirmed:
305                 break;
306             default:
307                 // Do nothing.
308         }
309     }
310 
updateUIWhenPatternInProgress()311     private void updateUIWhenPatternInProgress() {
312         mMessageText.setText(R.string.lockpattern_recording_inprogress);
313         setPrimaryButtonEnabled(false);
314         setSecondaryButtonEnabled(false);
315     }
316 
317     // clear the wrong pattern unless they have started a new one
318     // already
postClearPatternRunnable()319     private void postClearPatternRunnable() {
320         mLockPatternView.removeCallbacks(mClearPatternRunnable);
321         mLockPatternView.postDelayed(mClearPatternRunnable, mWrongPatternClearTimeOut);
322     }
323 
setPrimaryButtonEnabled(boolean enabled)324     private void setPrimaryButtonEnabled(boolean enabled) {
325         mPrimaryButton.setEnabled(enabled);
326     }
327 
setPrimaryButtonText(@tringRes int textId)328     private void setPrimaryButtonText(@StringRes int textId) {
329         mPrimaryButton.setTitle(textId);
330     }
331 
setSecondaryButtonVisible(boolean visible)332     private void setSecondaryButtonVisible(boolean visible) {
333         mSecondaryButton.setVisible(visible);
334     }
335 
setSecondaryButtonEnabled(boolean enabled)336     private void setSecondaryButtonEnabled(boolean enabled) {
337         mSecondaryButton.setEnabled(enabled);
338     }
339 
setSecondaryButtonText(@tringRes int textId)340     private void setSecondaryButtonText(@StringRes int textId) {
341         mSecondaryButton.setTitle(textId);
342     }
343 
344     // Update display message and decide on next step according to the different mText
345     // on the primary button
handlePrimaryButtonClick()346     private void handlePrimaryButtonClick() {
347         switch (mUiStage.mPrimaryButtonState) {
348             case Continue:
349                 if (mUiStage != Stage.FirstChoiceValid) {
350                     throw new IllegalStateException("expected ui stage "
351                             + Stage.FirstChoiceValid + " when button is "
352                             + PrimaryButtonState.Continue);
353                 }
354                 updateStage(Stage.NeedToConfirm);
355                 break;
356             case Confirm:
357                 if (mUiStage != Stage.ChoiceConfirmed) {
358                     throw new IllegalStateException("expected ui stage " + Stage.ChoiceConfirmed
359                             + " when button is " + PrimaryButtonState.Confirm);
360                 }
361                 startSaveAndFinish();
362                 break;
363             case Retry:
364                 if (mUiStage != Stage.SaveFailure) {
365                     throw new IllegalStateException("expected ui stage " + Stage.SaveFailure
366                             + " when button is " + PrimaryButtonState.Retry);
367                 }
368                 startSaveAndFinish();
369                 break;
370             case Ok:
371                 if (mUiStage != Stage.HelpScreen) {
372                     throw new IllegalStateException("Help screen is only mode with ok button, "
373                             + "but stage is " + mUiStage);
374                 }
375                 mLockPatternView.clearPattern();
376                 mLockPatternView.setDisplayMode(DisplayMode.Correct);
377                 updateStage(Stage.Introduction);
378                 break;
379             default:
380                 // Do nothing.
381         }
382     }
383 
384     // Update display message and proceed to next step according to the different mText on
385     // the secondary button.
handleSecondaryButtonClick()386     private void handleSecondaryButtonClick() {
387         if (mUiStage.mSecondaryButtonState == SecondaryButtonState.Retry) {
388             mChosenPattern = null;
389             mLockPatternView.clearPattern();
390             updateStage(Stage.Introduction);
391         } else {
392             throw new IllegalStateException("secondary button pressed, but stage of "
393                     + mUiStage + " doesn't make sense");
394         }
395     }
396 
397     @VisibleForTesting
onChosenLockSaveFinished(boolean isSaveSuccessful)398     void onChosenLockSaveFinished(boolean isSaveSuccessful) {
399         getToolbar().getProgressBar().setVisible(false);
400 
401         if (isSaveSuccessful) {
402             onComplete();
403         } else {
404             updateStage(Stage.SaveFailure);
405         }
406     }
407 
408     // Save recorded pattern as an async task and proceed to next
startSaveAndFinish()409     private void startSaveAndFinish() {
410         if (mSaveLockWorker != null && !mSaveLockWorker.isFinished()) {
411             LOG.v("startSaveAndFinish with a running SavePatternWorker.");
412             return;
413         }
414 
415         setPrimaryButtonEnabled(false);
416 
417         if (mSaveLockWorker == null) {
418             mSaveLockWorker = new SaveLockWorker();
419             mSaveLockWorker.setListener(this::onChosenLockSaveFinished);
420 
421             getFragmentManager()
422                     .beginTransaction()
423                     .add(mSaveLockWorker, FRAGMENT_TAG_SAVE_PATTERN_WORKER)
424                     .commitNow();
425         }
426 
427         mSaveLockWorker.start(mUserId, mChosenPattern, mCurrentCredential);
428         getToolbar().getProgressBar().setVisible(true);
429     }
430 
431     @VisibleForTesting
onComplete()432     void onComplete() {
433         if (mCurrentCredential != null) {
434             mCurrentCredential.zeroize();
435         }
436 
437         getActivity().setResult(Activity.RESULT_OK);
438         getActivity().finish();
439     }
440 
441     /**
442      * Keep track internally of where the user is in choosing a pattern.
443      */
444     enum Stage {
445         /**
446          * Initial stage when first launching choose a lock pattern.
447          * Pattern mEnabled, secondary button allow for Cancel, primary button disabled.
448          */
449         Introduction(
450                 R.string.lockpattern_recording_intro_header,
451                 SecondaryButtonState.Gone,
452                 PrimaryButtonState.ContinueDisabled,
453                 /* patternEnabled= */ true),
454         /**
455          * Help screen to show how a valid pattern looks like.
456          * Pattern disabled, primary button shows Ok. No secondary button.
457          */
458         HelpScreen(
459                 R.string.lockpattern_settings_help_how_to_record,
460                 SecondaryButtonState.Gone,
461                 PrimaryButtonState.Ok,
462                 /* patternEnabled= */ false),
463         /**
464          * Invalid pattern is entered, hint message show required number of dots.
465          * Secondary button allows for Retry, primary button disabled.
466          */
467         ChoiceTooShort(
468                 R.string.lockpattern_recording_incorrect_too_short,
469                 SecondaryButtonState.Retry,
470                 PrimaryButtonState.ContinueDisabled,
471                 /* patternEnabled= */ true),
472         /**
473          * First drawing on the pattern is valid, primary button shows Continue,
474          * can proceed to next screen.
475          */
476         FirstChoiceValid(
477                 R.string.lockpattern_recording_intro_header,
478                 SecondaryButtonState.Retry,
479                 PrimaryButtonState.Continue,
480                 /* patternEnabled= */ false),
481         /**
482          * Need to draw pattern again to confirm.
483          * Secondary button allows for Cancel, primary button disabled.
484          */
485         NeedToConfirm(
486                 R.string.lockpattern_need_to_confirm,
487                 SecondaryButtonState.Gone,
488                 PrimaryButtonState.ConfirmDisabled,
489                 /* patternEnabled= */ true),
490         /**
491          * Confirmation of previous drawn pattern failed, didn't enter the same pattern.
492          * Need to re-draw the pattern to match the fist pattern.
493          */
494         ConfirmWrong(
495                 R.string.lockpattern_pattern_wrong,
496                 SecondaryButtonState.Gone,
497                 PrimaryButtonState.ConfirmDisabled,
498                 /* patternEnabled= */ true),
499         /**
500          * Pattern is confirmed after drawing the same pattern twice.
501          * Pattern disabled.
502          */
503         ChoiceConfirmed(
504                 R.string.lockpattern_pattern_confirmed,
505                 SecondaryButtonState.Gone,
506                 PrimaryButtonState.Confirm,
507                 /* patternEnabled= */ false),
508 
509         /**
510          * Error saving pattern.
511          * Pattern disabled, primary button shows Retry, secondary button allows for cancel
512          */
513         SaveFailure(
514                 R.string.error_saving_lockpattern,
515                 SecondaryButtonState.Gone,
516                 PrimaryButtonState.Retry,
517                 /* patternEnabled= */ false);
518 
519         final int mMessageId;
520         final SecondaryButtonState mSecondaryButtonState;
521         final PrimaryButtonState mPrimaryButtonState;
522         final boolean mPatternEnabled;
523 
524         /**
525          * @param messageId            The message displayed as instruction.
526          * @param secondaryButtonState The state of the secondary button.
527          * @param primaryButtonState   The state of the primary button.
528          * @param patternEnabled       Whether the pattern widget is mEnabled.
529          */
Stage(@tringRes int messageId, SecondaryButtonState secondaryButtonState, PrimaryButtonState primaryButtonState, boolean patternEnabled)530         Stage(@StringRes int messageId,
531                 SecondaryButtonState secondaryButtonState,
532                 PrimaryButtonState primaryButtonState,
533                 boolean patternEnabled) {
534             this.mMessageId = messageId;
535             this.mSecondaryButtonState = secondaryButtonState;
536             this.mPrimaryButtonState = primaryButtonState;
537             this.mPatternEnabled = patternEnabled;
538         }
539     }
540 
541     /**
542      * The states of the primary footer button.
543      */
544     enum PrimaryButtonState {
545         Continue(R.string.continue_button_text, true),
546         ContinueDisabled(R.string.continue_button_text, false),
547         Confirm(R.string.lockpattern_confirm_button_text, true),
548         ConfirmDisabled(R.string.lockpattern_confirm_button_text, false),
549         Retry(R.string.lockscreen_retry_button_text, true),
550         Ok(R.string.okay, true);
551 
552         final int mText;
553         final boolean mEnabled;
554 
555         /**
556          * @param text    The displayed mText for this mode.
557          * @param enabled Whether the button should be mEnabled.
558          */
PrimaryButtonState(@tringRes int text, boolean enabled)559         PrimaryButtonState(@StringRes int text, boolean enabled) {
560             this.mText = text;
561             this.mEnabled = enabled;
562         }
563     }
564 
565     /**
566      * The states of the secondary footer button.
567      */
568     enum SecondaryButtonState {
569         Retry(R.string.lockpattern_retry_button_text, true),
570         Gone(ID_EMPTY_MESSAGE, false);
571 
572         final int mTextResId;
573         final boolean mEnabled;
574 
575         /**
576          * @param textId  The displayed mText for this mode.
577          * @param enabled Whether the button should be mEnabled.
578          */
SecondaryButtonState(@tringRes int textId, boolean enabled)579         SecondaryButtonState(@StringRes int textId, boolean enabled) {
580             this.mTextResId = textId;
581             this.mEnabled = enabled;
582         }
583     }
584 }
585