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