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