ChooseLockPassword.java revision 9990f397220703f4d2c922560a8e29e60bcce39f
1/*
2 * Copyright (C) 2010 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
17package com.android.settings.password;
18
19import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
20import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
21import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
22import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
23import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
24
25import android.app.Activity;
26import android.app.Fragment;
27import android.app.admin.DevicePolicyManager;
28import android.app.admin.PasswordMetrics;
29import android.content.Context;
30import android.content.Intent;
31import android.content.res.Resources.Theme;
32import android.graphics.Insets;
33import android.os.Bundle;
34import android.os.Handler;
35import android.os.Message;
36import android.support.annotation.StringRes;
37import android.support.v7.widget.LinearLayoutManager;
38import android.support.v7.widget.RecyclerView;
39import android.text.Editable;
40import android.text.InputType;
41import android.text.Selection;
42import android.text.Spannable;
43import android.text.TextUtils;
44import android.text.TextWatcher;
45import android.util.Log;
46import android.view.KeyEvent;
47import android.view.LayoutInflater;
48import android.view.View;
49import android.view.View.OnClickListener;
50import android.view.ViewGroup;
51import android.view.inputmethod.EditorInfo;
52import android.widget.Button;
53import android.widget.EditText;
54import android.widget.LinearLayout;
55import android.widget.TextView;
56import android.widget.TextView.OnEditorActionListener;
57
58import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
59import com.android.internal.widget.LockPatternUtils;
60import com.android.internal.widget.LockPatternUtils.RequestThrottledException;
61import com.android.internal.widget.TextViewInputDisabler;
62import com.android.settings.EncryptionInterstitial;
63import com.android.settings.R;
64import com.android.settings.SettingsActivity;
65import com.android.settings.SetupWizardUtils;
66import com.android.settings.Utils;
67import com.android.settings.core.InstrumentedPreferenceFragment;
68import com.android.settings.notification.RedactionInterstitial;
69import com.android.setupwizardlib.GlifLayout;
70
71import java.util.ArrayList;
72import java.util.List;
73
74public class ChooseLockPassword extends SettingsActivity {
75    public static final String PASSWORD_MIN_KEY = "lockscreen.password_min";
76    public static final String PASSWORD_MAX_KEY = "lockscreen.password_max";
77    public static final String PASSWORD_MIN_LETTERS_KEY = "lockscreen.password_min_letters";
78    public static final String PASSWORD_MIN_LOWERCASE_KEY = "lockscreen.password_min_lowercase";
79    public static final String PASSWORD_MIN_UPPERCASE_KEY = "lockscreen.password_min_uppercase";
80    public static final String PASSWORD_MIN_NUMERIC_KEY = "lockscreen.password_min_numeric";
81    public static final String PASSWORD_MIN_SYMBOLS_KEY = "lockscreen.password_min_symbols";
82    public static final String PASSWORD_MIN_NONLETTER_KEY = "lockscreen.password_min_nonletter";
83
84    private static final String TAG = "ChooseLockPassword";
85
86    @Override
87    public Intent getIntent() {
88        Intent modIntent = new Intent(super.getIntent());
89        modIntent.putExtra(EXTRA_SHOW_FRAGMENT, getFragmentClass().getName());
90        return modIntent;
91    }
92
93    @Override
94    protected void onApplyThemeResource(Theme theme, int resid, boolean first) {
95        resid = SetupWizardUtils.getTheme(getIntent());
96        super.onApplyThemeResource(theme, resid, first);
97    }
98
99    public static class IntentBuilder {
100
101        private final Intent mIntent;
102
103        public IntentBuilder(Context context) {
104            mIntent = new Intent(context, ChooseLockPassword.class);
105            mIntent.putExtra(ChooseLockGeneric.CONFIRM_CREDENTIALS, false);
106            mIntent.putExtra(EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, false);
107        }
108
109        public IntentBuilder setPasswordQuality(int quality) {
110            mIntent.putExtra(LockPatternUtils.PASSWORD_TYPE_KEY, quality);
111            return this;
112        }
113
114        public IntentBuilder setPasswordLengthRange(int min, int max) {
115            mIntent.putExtra(PASSWORD_MIN_KEY, min);
116            mIntent.putExtra(PASSWORD_MAX_KEY, max);
117            return this;
118        }
119
120        public IntentBuilder setUserId(int userId) {
121            mIntent.putExtra(Intent.EXTRA_USER_ID, userId);
122            return this;
123        }
124
125        public IntentBuilder setChallenge(long challenge) {
126            mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, true);
127            mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, challenge);
128            return this;
129        }
130
131        public IntentBuilder setPassword(String password) {
132            mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, false);
133            mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, password);
134            return this;
135        }
136
137        public IntentBuilder setForFingerprint(boolean forFingerprint) {
138            mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, forFingerprint);
139            return this;
140        }
141
142        public Intent build() {
143            return mIntent;
144        }
145    }
146
147    @Override
148    protected boolean isValidFragment(String fragmentName) {
149        if (ChooseLockPasswordFragment.class.getName().equals(fragmentName)) return true;
150        return false;
151    }
152
153    /* package */ Class<? extends Fragment> getFragmentClass() {
154        return ChooseLockPasswordFragment.class;
155    }
156
157    @Override
158    protected void onCreate(Bundle savedInstanceState) {
159        super.onCreate(savedInstanceState);
160        boolean forFingerprint = getIntent()
161                .getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, false);
162        CharSequence msg = getText(forFingerprint
163                ? R.string.lockpassword_choose_your_password_header_for_fingerprint
164                : R.string.lockpassword_choose_your_password_header);
165        setTitle(msg);
166        LinearLayout layout = (LinearLayout) findViewById(R.id.content_parent);
167        layout.setFitsSystemWindows(false);
168    }
169
170    public static class ChooseLockPasswordFragment extends InstrumentedPreferenceFragment
171            implements OnClickListener, OnEditorActionListener, TextWatcher,
172            SaveAndFinishWorker.Listener {
173        private static final String KEY_FIRST_PIN = "first_pin";
174        private static final String KEY_UI_STAGE = "ui_stage";
175        private static final String KEY_CURRENT_PASSWORD = "current_password";
176        private static final String FRAGMENT_TAG_SAVE_AND_FINISH = "save_and_finish_worker";
177
178        private String mCurrentPassword;
179        private String mChosenPassword;
180        private boolean mHasChallenge;
181        private long mChallenge;
182        private EditText mPasswordEntry;
183        private TextViewInputDisabler mPasswordEntryInputDisabler;
184        private int mPasswordMinLength = LockPatternUtils.MIN_LOCK_PASSWORD_SIZE;
185        private int mPasswordMaxLength = 16;
186        private int mPasswordMinLetters = 0;
187        private int mPasswordMinUpperCase = 0;
188        private int mPasswordMinLowerCase = 0;
189        private int mPasswordMinSymbols = 0;
190        private int mPasswordMinNumeric = 0;
191        private int mPasswordMinNonLetter = 0;
192        private int mPasswordMinLengthToFulfillAllPolicies = 0;
193        protected int mUserId;
194        private boolean mHideDrawer = false;
195        /**
196         * Password requirements that we need to verify.
197         */
198        private int[] mPasswordRequirements;
199
200        private LockPatternUtils mLockPatternUtils;
201        private SaveAndFinishWorker mSaveAndFinishWorker;
202        private int mRequestedQuality = DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
203        private ChooseLockSettingsHelper mChooseLockSettingsHelper;
204        private Stage mUiStage = Stage.Introduction;
205        private PasswordRequirementAdapter mPasswordRequirementAdapter;
206        private GlifLayout mLayout;
207        private boolean mForFingerprint;
208
209        private String mFirstPin;
210        private RecyclerView mPasswordRestrictionView;
211        protected boolean mIsAlphaMode;
212        protected Button mCancelButton;
213        private Button mNextButton;
214
215        private TextChangedHandler mTextChangedHandler;
216
217        private static final int CONFIRM_EXISTING_REQUEST = 58;
218        static final int RESULT_FINISHED = RESULT_FIRST_USER;
219
220        private static final int MIN_LETTER_IN_PASSWORD = 0;
221        private static final int MIN_UPPER_LETTERS_IN_PASSWORD = 1;
222        private static final int MIN_LOWER_LETTERS_IN_PASSWORD = 2;
223        private static final int MIN_SYMBOLS_IN_PASSWORD = 3;
224        private static final int MIN_NUMBER_IN_PASSWORD = 4;
225        private static final int MIN_NON_LETTER_IN_PASSWORD = 5;
226
227        // Error code returned from {@link #validatePassword(String)}.
228        private static final int NO_ERROR = 0;
229        private static final int CONTAIN_INVALID_CHARACTERS = 1 << 0;
230        private static final int TOO_SHORT = 1 << 1;
231        private static final int TOO_LONG = 1 << 2;
232        private static final int CONTAIN_NON_DIGITS = 1 << 3;
233        private static final int CONTAIN_SEQUENTIAL_DIGITS = 1 << 4;
234        private static final int RECENTLY_USED = 1 << 5;
235        private static final int NOT_ENOUGH_LETTER = 1 << 6;
236        private static final int NOT_ENOUGH_UPPER_CASE = 1 << 7;
237        private static final int NOT_ENOUGH_LOWER_CASE = 1 << 8;
238        private static final int NOT_ENOUGH_DIGITS = 1 << 9;
239        private static final int NOT_ENOUGH_SYMBOLS = 1 << 10;
240        private static final int NOT_ENOUGH_NON_LETTER = 1 << 11;
241
242        /**
243         * Keep track internally of where the user is in choosing a pattern.
244         */
245        protected enum Stage {
246
247            Introduction(
248                    R.string.lockpassword_choose_your_password_header,
249                    R.string.lockpassword_choose_your_password_header_for_fingerprint,
250                    R.string.lockpassword_choose_your_pin_header,
251                    R.string.lockpassword_choose_your_pin_header_for_fingerprint,
252                    R.string.next_label),
253
254            NeedToConfirm(
255                    R.string.lockpassword_confirm_your_password_header,
256                    R.string.lockpassword_confirm_your_password_header,
257                    R.string.lockpassword_confirm_your_pin_header,
258                    R.string.lockpassword_confirm_your_pin_header,
259                    R.string.lockpassword_ok_label),
260
261            ConfirmWrong(
262                    R.string.lockpassword_confirm_passwords_dont_match,
263                    R.string.lockpassword_confirm_passwords_dont_match,
264                    R.string.lockpassword_confirm_pins_dont_match,
265                    R.string.lockpassword_confirm_pins_dont_match,
266                    R.string.next_label);
267
268            Stage(int hintInAlpha, int hintInAlphaForFingerprint,
269                    int hintInNumeric, int hintInNumericForFingerprint, int nextButtonText) {
270                this.alphaHint = hintInAlpha;
271                this.alphaHintForFingerprint = hintInAlphaForFingerprint;
272                this.numericHint = hintInNumeric;
273                this.numericHintForFingerprint = hintInNumericForFingerprint;
274                this.buttonText = nextButtonText;
275            }
276
277            public final int alphaHint;
278            public final int alphaHintForFingerprint;
279            public final int numericHint;
280            public final int numericHintForFingerprint;
281            public final int buttonText;
282
283            public @StringRes int getHint(boolean isAlpha, boolean isFingerprint) {
284                if (isAlpha) {
285                    return isFingerprint ? alphaHintForFingerprint : alphaHint;
286                } else {
287                    return isFingerprint ? numericHintForFingerprint : numericHint;
288                }
289            }
290        }
291
292        // required constructor for fragments
293        public ChooseLockPasswordFragment() {
294
295        }
296
297        @Override
298        public void onCreate(Bundle savedInstanceState) {
299            super.onCreate(savedInstanceState);
300            mLockPatternUtils = new LockPatternUtils(getActivity());
301            Intent intent = getActivity().getIntent();
302            if (!(getActivity() instanceof ChooseLockPassword)) {
303                throw new SecurityException("Fragment contained in wrong activity");
304            }
305            // Only take this argument into account if it belongs to the current profile.
306            mUserId = Utils.getUserIdFromBundle(getActivity(), intent.getExtras());
307            mForFingerprint = intent.getBooleanExtra(
308                    ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, false);
309            processPasswordRequirements(intent);
310            mChooseLockSettingsHelper = new ChooseLockSettingsHelper(getActivity());
311            mHideDrawer = getActivity().getIntent().getBooleanExtra(EXTRA_HIDE_DRAWER, false);
312
313            if (intent.getBooleanExtra(
314                    ChooseLockSettingsHelper.EXTRA_KEY_FOR_CHANGE_CRED_REQUIRED_FOR_BOOT, false)) {
315                SaveAndFinishWorker w = new SaveAndFinishWorker();
316                final boolean required = getActivity().getIntent().getBooleanExtra(
317                        EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true);
318                String current = intent.getStringExtra(
319                        ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD);
320                w.setBlocking(true);
321                w.setListener(this);
322                w.start(mChooseLockSettingsHelper.utils(), required,
323                        false, 0, current, current, mRequestedQuality, mUserId);
324            }
325            mTextChangedHandler = new TextChangedHandler();
326        }
327
328        @Override
329        public View onCreateView(LayoutInflater inflater, ViewGroup container,
330                Bundle savedInstanceState) {
331            return inflater.inflate(R.layout.choose_lock_password, container, false);
332        }
333
334        @Override
335        public void onViewCreated(View view, Bundle savedInstanceState) {
336            super.onViewCreated(view, savedInstanceState);
337
338            mLayout = (GlifLayout) view;
339
340            // Make the password container consume the optical insets so the edit text is aligned
341            // with the sides of the parent visually.
342            ViewGroup container = view.findViewById(R.id.password_container);
343            container.setOpticalInsets(Insets.NONE);
344
345            mCancelButton = (Button) view.findViewById(R.id.cancel_button);
346            mCancelButton.setOnClickListener(this);
347            mNextButton = (Button) view.findViewById(R.id.next_button);
348            mNextButton.setOnClickListener(this);
349
350            if (mForFingerprint) {
351                TextView fingerprintBackupMessage =
352                        view.findViewById(R.id.fingerprint_backup_message);
353                if (fingerprintBackupMessage != null) {
354                    fingerprintBackupMessage.setVisibility(View.VISIBLE);
355                    fingerprintBackupMessage
356                            .setText(R.string.setup_lock_settings_picker_fingerprint_message);
357                }
358            }
359
360            mIsAlphaMode = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == mRequestedQuality
361                    || DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == mRequestedQuality
362                    || DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == mRequestedQuality;
363
364            setupPasswordRequirementsView(view);
365
366            mPasswordRestrictionView.setLayoutManager(new LinearLayoutManager(getActivity()));
367            mPasswordEntry = (EditText) view.findViewById(R.id.password_entry);
368            mPasswordEntry.setOnEditorActionListener(this);
369            mPasswordEntry.addTextChangedListener(this);
370            mPasswordEntry.requestFocus();
371            mPasswordEntryInputDisabler = new TextViewInputDisabler(mPasswordEntry);
372
373            final Activity activity = getActivity();
374
375            int currentType = mPasswordEntry.getInputType();
376            mPasswordEntry.setInputType(mIsAlphaMode ? currentType
377                    : (InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD));
378
379            Intent intent = getActivity().getIntent();
380            final boolean confirmCredentials = intent.getBooleanExtra(
381                    ChooseLockGeneric.CONFIRM_CREDENTIALS, true);
382            mCurrentPassword = intent.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD);
383            mHasChallenge = intent.getBooleanExtra(
384                    ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, false);
385            mChallenge = intent.getLongExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, 0);
386            if (savedInstanceState == null) {
387                updateStage(Stage.Introduction);
388                if (confirmCredentials) {
389                    mChooseLockSettingsHelper.launchConfirmationActivity(CONFIRM_EXISTING_REQUEST,
390                            getString(R.string.unlock_set_unlock_launch_picker_title), true,
391                            mUserId);
392                }
393            } else {
394                // restore from previous state
395                mFirstPin = savedInstanceState.getString(KEY_FIRST_PIN);
396                final String state = savedInstanceState.getString(KEY_UI_STAGE);
397                if (state != null) {
398                    mUiStage = Stage.valueOf(state);
399                    updateStage(mUiStage);
400                }
401
402                if (mCurrentPassword == null) {
403                    mCurrentPassword = savedInstanceState.getString(KEY_CURRENT_PASSWORD);
404                }
405
406                // Re-attach to the exiting worker if there is one.
407                mSaveAndFinishWorker = (SaveAndFinishWorker) getFragmentManager().findFragmentByTag(
408                        FRAGMENT_TAG_SAVE_AND_FINISH);
409            }
410
411            if (activity instanceof SettingsActivity) {
412                final SettingsActivity sa = (SettingsActivity) activity;
413                int title = Stage.Introduction.getHint(mIsAlphaMode, mForFingerprint);
414                sa.setTitle(title);
415                mLayout.setHeaderText(title);
416            }
417        }
418
419        private void setupPasswordRequirementsView(View view) {
420            // Construct passwordRequirements and requirementDescriptions.
421            List<Integer> passwordRequirements = new ArrayList<>();
422            List<String> requirementDescriptions = new ArrayList<>();
423            if (mPasswordMinUpperCase > 0) {
424                passwordRequirements.add(MIN_UPPER_LETTERS_IN_PASSWORD);
425                requirementDescriptions.add(getResources().getQuantityString(
426                        R.plurals.lockpassword_password_requires_uppercase, mPasswordMinUpperCase,
427                        mPasswordMinUpperCase));
428            }
429            if (mPasswordMinLowerCase > 0) {
430                passwordRequirements.add(MIN_LOWER_LETTERS_IN_PASSWORD);
431                requirementDescriptions.add(getResources().getQuantityString(
432                        R.plurals.lockpassword_password_requires_lowercase, mPasswordMinLowerCase,
433                        mPasswordMinLowerCase));
434            }
435            if (mPasswordMinLetters > 0) {
436                if (mPasswordMinLetters > mPasswordMinUpperCase + mPasswordMinLowerCase) {
437                    passwordRequirements.add(MIN_LETTER_IN_PASSWORD);
438                    requirementDescriptions.add(getResources().getQuantityString(
439                            R.plurals.lockpassword_password_requires_letters, mPasswordMinLetters,
440                            mPasswordMinLetters));
441                }
442            }
443            if (mPasswordMinNumeric > 0) {
444                passwordRequirements.add(MIN_NUMBER_IN_PASSWORD);
445                requirementDescriptions.add(getResources().getQuantityString(
446                        R.plurals.lockpassword_password_requires_numeric, mPasswordMinNumeric,
447                        mPasswordMinNumeric));
448            }
449            if (mPasswordMinSymbols > 0) {
450                passwordRequirements.add(MIN_SYMBOLS_IN_PASSWORD);
451                requirementDescriptions.add(getResources().getQuantityString(
452                        R.plurals.lockpassword_password_requires_symbols, mPasswordMinSymbols,
453                        mPasswordMinSymbols));
454            }
455            if (mPasswordMinNonLetter > 0) {
456                if (mPasswordMinNonLetter > mPasswordMinNumeric + mPasswordMinSymbols) {
457                    passwordRequirements.add(MIN_NON_LETTER_IN_PASSWORD);
458                    requirementDescriptions.add(getResources().getQuantityString(
459                            R.plurals.lockpassword_password_requires_nonletter, mPasswordMinNonLetter,
460
461                            mPasswordMinNonLetter));
462                }
463            }
464            // Convert list to array.
465            mPasswordRequirements = passwordRequirements.stream().mapToInt(i -> i).toArray();
466            mPasswordRestrictionView =
467                    (RecyclerView) view.findViewById(R.id.password_requirements_view);
468            mPasswordRestrictionView.setLayoutManager(new LinearLayoutManager(getActivity()));
469            mPasswordRequirementAdapter = new PasswordRequirementAdapter();
470            mPasswordRestrictionView.setAdapter(mPasswordRequirementAdapter);
471        }
472
473        @Override
474        public int getMetricsCategory() {
475            return MetricsEvent.CHOOSE_LOCK_PASSWORD;
476        }
477
478        @Override
479        public void onResume() {
480            super.onResume();
481            updateStage(mUiStage);
482            if (mSaveAndFinishWorker != null) {
483                mSaveAndFinishWorker.setListener(this);
484            } else {
485                mPasswordEntry.requestFocus();
486            }
487        }
488
489        @Override
490        public void onPause() {
491            if (mSaveAndFinishWorker != null) {
492                mSaveAndFinishWorker.setListener(null);
493            }
494            super.onPause();
495        }
496
497        @Override
498        public void onSaveInstanceState(Bundle outState) {
499            super.onSaveInstanceState(outState);
500            outState.putString(KEY_UI_STAGE, mUiStage.name());
501            outState.putString(KEY_FIRST_PIN, mFirstPin);
502            outState.putString(KEY_CURRENT_PASSWORD, mCurrentPassword);
503        }
504
505        @Override
506        public void onActivityResult(int requestCode, int resultCode,
507                Intent data) {
508            super.onActivityResult(requestCode, resultCode, data);
509            switch (requestCode) {
510                case CONFIRM_EXISTING_REQUEST:
511                    if (resultCode != Activity.RESULT_OK) {
512                        getActivity().setResult(RESULT_FINISHED);
513                        getActivity().finish();
514                    } else {
515                        mCurrentPassword = data.getStringExtra(
516                                ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD);
517                    }
518                    break;
519            }
520        }
521
522        protected Intent getRedactionInterstitialIntent(Context context) {
523            return RedactionInterstitial.createStartIntent(context, mUserId);
524        }
525
526        protected void updateStage(Stage stage) {
527            final Stage previousStage = mUiStage;
528            mUiStage = stage;
529            updateUi();
530
531            // If the stage changed, announce the header for accessibility. This
532            // is a no-op when accessibility is disabled.
533            if (previousStage != stage) {
534                mLayout.announceForAccessibility(mLayout.getHeaderText());
535            }
536        }
537
538        /**
539         * Read the requirements from {@link DevicePolicyManager} and intent and aggregate them.
540         *
541         * @param intent the incoming intent
542         */
543        private void processPasswordRequirements(Intent intent) {
544            final int dpmPasswordQuality = mLockPatternUtils.getRequestedPasswordQuality(mUserId);
545            mRequestedQuality = Math.max(intent.getIntExtra(LockPatternUtils.PASSWORD_TYPE_KEY,
546                    mRequestedQuality), dpmPasswordQuality);
547            mPasswordMinLength = Math.max(Math.max(
548                    LockPatternUtils.MIN_LOCK_PASSWORD_SIZE,
549                    intent.getIntExtra(PASSWORD_MIN_KEY, mPasswordMinLength)),
550                    mLockPatternUtils.getRequestedMinimumPasswordLength(mUserId));
551            mPasswordMaxLength = intent.getIntExtra(PASSWORD_MAX_KEY, mPasswordMaxLength);
552            mPasswordMinLetters = Math.max(intent.getIntExtra(PASSWORD_MIN_LETTERS_KEY,
553                    mPasswordMinLetters), mLockPatternUtils.getRequestedPasswordMinimumLetters(
554                    mUserId));
555            mPasswordMinUpperCase = Math.max(intent.getIntExtra(PASSWORD_MIN_UPPERCASE_KEY,
556                    mPasswordMinUpperCase), mLockPatternUtils.getRequestedPasswordMinimumUpperCase(
557                    mUserId));
558            mPasswordMinLowerCase = Math.max(intent.getIntExtra(PASSWORD_MIN_LOWERCASE_KEY,
559                    mPasswordMinLowerCase), mLockPatternUtils.getRequestedPasswordMinimumLowerCase(
560                    mUserId));
561            mPasswordMinNumeric = Math.max(intent.getIntExtra(PASSWORD_MIN_NUMERIC_KEY,
562                    mPasswordMinNumeric), mLockPatternUtils.getRequestedPasswordMinimumNumeric(
563                    mUserId));
564            mPasswordMinSymbols = Math.max(intent.getIntExtra(PASSWORD_MIN_SYMBOLS_KEY,
565                    mPasswordMinSymbols), mLockPatternUtils.getRequestedPasswordMinimumSymbols(
566                    mUserId));
567            mPasswordMinNonLetter = Math.max(intent.getIntExtra(PASSWORD_MIN_NONLETTER_KEY,
568                    mPasswordMinNonLetter), mLockPatternUtils.getRequestedPasswordMinimumNonLetter(
569                    mUserId));
570
571            // Modify the value based on dpm policy.
572            switch (dpmPasswordQuality) {
573                case PASSWORD_QUALITY_ALPHABETIC:
574                    if (mPasswordMinLetters == 0) {
575                        mPasswordMinLetters = 1;
576                    }
577                    break;
578                case PASSWORD_QUALITY_ALPHANUMERIC:
579                    if (mPasswordMinLetters == 0) {
580                        mPasswordMinLetters = 1;
581                    }
582                    if (mPasswordMinNumeric == 0) {
583                        mPasswordMinNumeric = 1;
584                    }
585                    break;
586                case PASSWORD_QUALITY_COMPLEX:
587                    // Reserve all the requirements.
588                    break;
589                default:
590                    mPasswordMinNumeric = 0;
591                    mPasswordMinLetters = 0;
592                    mPasswordMinUpperCase = 0;
593                    mPasswordMinLowerCase = 0;
594                    mPasswordMinSymbols = 0;
595                    mPasswordMinNonLetter = 0;
596            }
597            mPasswordMinLengthToFulfillAllPolicies = getMinLengthToFulfillAllPolicies();
598        }
599
600        /**
601         * Validates PIN and returns the validation result.
602         *
603         * @param password the raw password the user typed in
604         * @return the validation result.
605         */
606        private int validatePassword(String password) {
607            int errorCode = NO_ERROR;
608
609            if (password.length() < mPasswordMinLength) {
610                if (mPasswordMinLength > mPasswordMinLengthToFulfillAllPolicies) {
611                    errorCode |= TOO_SHORT;
612                }
613            } else if (password.length() > mPasswordMaxLength) {
614                errorCode |= TOO_LONG;
615            } else {
616                // The length requirements are fulfilled.
617                if (mRequestedQuality == PASSWORD_QUALITY_NUMERIC_COMPLEX) {
618                    // Check for repeated characters or sequences (e.g. '1234', '0000', '2468')
619                    final int sequence = PasswordMetrics.maxLengthSequence(password);
620                    if (sequence > PasswordMetrics.MAX_ALLOWED_SEQUENCE) {
621                        errorCode |= CONTAIN_SEQUENTIAL_DIGITS;
622                    }
623                }
624                // Is the password recently used?
625                if (mLockPatternUtils.checkPasswordHistory(password, mUserId)) {
626                    errorCode |= RECENTLY_USED;
627                }
628            }
629
630            // Allow non-control Latin-1 characters only.
631            for (int i = 0; i < password.length(); i++) {
632                char c = password.charAt(i);
633                if (c < 32 || c > 127) {
634                    errorCode |= CONTAIN_INVALID_CHARACTERS;
635                    break;
636                }
637            }
638
639            final PasswordMetrics metrics = PasswordMetrics.computeForPassword(password);
640
641            // Ensure no non-digits if we are requesting numbers. This shouldn't be possible unless
642            // user finds some way to bring up soft keyboard.
643            if (mRequestedQuality == PASSWORD_QUALITY_NUMERIC
644                    || mRequestedQuality == PASSWORD_QUALITY_NUMERIC_COMPLEX) {
645                if (metrics.letters > 0 || metrics.symbols > 0) {
646                    errorCode |= CONTAIN_NON_DIGITS;
647                }
648            }
649
650            // Check the requirements one by one.
651            for (int i = 0; i < mPasswordRequirements.length; i++) {
652                int passwordRestriction = mPasswordRequirements[i];
653                switch (passwordRestriction) {
654                    case MIN_LETTER_IN_PASSWORD:
655                        if (metrics.letters < mPasswordMinLetters) {
656                            errorCode |= NOT_ENOUGH_LETTER;
657                        }
658                        break;
659                    case MIN_UPPER_LETTERS_IN_PASSWORD:
660                        if (metrics.upperCase < mPasswordMinUpperCase) {
661                            errorCode |= NOT_ENOUGH_UPPER_CASE;
662                        }
663                        break;
664                    case MIN_LOWER_LETTERS_IN_PASSWORD:
665                        if (metrics.lowerCase < mPasswordMinLowerCase) {
666                            errorCode |= NOT_ENOUGH_LOWER_CASE;
667                        }
668                        break;
669                    case MIN_SYMBOLS_IN_PASSWORD:
670                        if (metrics.symbols < mPasswordMinSymbols) {
671                            errorCode |= NOT_ENOUGH_SYMBOLS;
672                        }
673                        break;
674                    case MIN_NUMBER_IN_PASSWORD:
675                        if (metrics.numeric < mPasswordMinNumeric) {
676                            errorCode |= NOT_ENOUGH_DIGITS;
677                        }
678                        break;
679                    case MIN_NON_LETTER_IN_PASSWORD:
680                        if (metrics.nonLetter < mPasswordMinNonLetter) {
681                            errorCode |= NOT_ENOUGH_NON_LETTER;
682                        }
683                        break;
684                }
685            }
686            return errorCode;
687        }
688
689        public void handleNext() {
690            if (mSaveAndFinishWorker != null) return;
691            mChosenPassword = mPasswordEntry.getText().toString();
692            if (TextUtils.isEmpty(mChosenPassword)) {
693                return;
694            }
695            if (mUiStage == Stage.Introduction) {
696                if (validatePassword(mChosenPassword) == NO_ERROR) {
697                    mFirstPin = mChosenPassword;
698                    mPasswordEntry.setText("");
699                    updateStage(Stage.NeedToConfirm);
700                }
701            } else if (mUiStage == Stage.NeedToConfirm) {
702                if (mFirstPin.equals(mChosenPassword)) {
703                    startSaveAndFinish();
704                } else {
705                    CharSequence tmp = mPasswordEntry.getText();
706                    if (tmp != null) {
707                        Selection.setSelection((Spannable) tmp, 0, tmp.length());
708                    }
709                    updateStage(Stage.ConfirmWrong);
710                }
711            }
712        }
713
714        protected void setNextEnabled(boolean enabled) {
715            mNextButton.setEnabled(enabled);
716        }
717
718        protected void setNextText(int text) {
719            mNextButton.setText(text);
720        }
721
722        public void onClick(View v) {
723            switch (v.getId()) {
724                case R.id.next_button:
725                    handleNext();
726                    break;
727
728                case R.id.cancel_button:
729                    getActivity().finish();
730                    break;
731            }
732        }
733
734        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
735            // Check if this was the result of hitting the enter or "done" key
736            if (actionId == EditorInfo.IME_NULL
737                    || actionId == EditorInfo.IME_ACTION_DONE
738                    || actionId == EditorInfo.IME_ACTION_NEXT) {
739                handleNext();
740                return true;
741            }
742            return false;
743        }
744
745        /**
746         * @param errorCode error code returned from {@link #validatePassword(String)}.
747         * @return an array of messages describing the error, important messages come first.
748         */
749        private String[] convertErrorCodeToMessages(int errorCode) {
750            List<String> messages = new ArrayList<>();
751            if ((errorCode & CONTAIN_INVALID_CHARACTERS) > 0) {
752                messages.add(getString(R.string.lockpassword_illegal_character));
753            }
754            if ((errorCode & CONTAIN_NON_DIGITS) > 0) {
755                messages.add(getString(R.string.lockpassword_pin_contains_non_digits));
756            }
757            if ((errorCode & NOT_ENOUGH_UPPER_CASE) > 0) {
758                messages.add(getResources().getQuantityString(
759                        R.plurals.lockpassword_password_requires_uppercase, mPasswordMinUpperCase,
760                        mPasswordMinUpperCase));
761            }
762            if ((errorCode & NOT_ENOUGH_LOWER_CASE) > 0) {
763                messages.add(getResources().getQuantityString(
764                        R.plurals.lockpassword_password_requires_lowercase, mPasswordMinLowerCase,
765                        mPasswordMinLowerCase));
766            }
767            if ((errorCode & NOT_ENOUGH_LETTER) > 0) {
768                messages.add(getResources().getQuantityString(
769                        R.plurals.lockpassword_password_requires_letters, mPasswordMinLetters,
770                        mPasswordMinLetters));
771            }
772            if ((errorCode & NOT_ENOUGH_DIGITS) > 0) {
773                messages.add(getResources().getQuantityString(
774                        R.plurals.lockpassword_password_requires_numeric, mPasswordMinNumeric,
775                        mPasswordMinNumeric));
776            }
777            if ((errorCode & NOT_ENOUGH_SYMBOLS) > 0) {
778                messages.add(getResources().getQuantityString(
779                        R.plurals.lockpassword_password_requires_symbols, mPasswordMinSymbols,
780                        mPasswordMinSymbols));
781            }
782            if ((errorCode & NOT_ENOUGH_NON_LETTER) > 0) {
783                messages.add(getResources().getQuantityString(
784                        R.plurals.lockpassword_password_requires_nonletter, mPasswordMinNonLetter,
785                        mPasswordMinNonLetter));
786            }
787            if ((errorCode & TOO_SHORT) > 0) {
788                messages.add(getString(mIsAlphaMode ?
789                        R.string.lockpassword_password_too_short
790                        : R.string.lockpassword_pin_too_short, mPasswordMinLength));
791            }
792            if ((errorCode & TOO_LONG) > 0) {
793                messages.add(getString(mIsAlphaMode ?
794                        R.string.lockpassword_password_too_long
795                        : R.string.lockpassword_pin_too_long, mPasswordMaxLength + 1));
796            }
797            if ((errorCode & CONTAIN_SEQUENTIAL_DIGITS) > 0) {
798                messages.add(getString(R.string.lockpassword_pin_no_sequential_digits));
799            }
800            if ((errorCode & RECENTLY_USED) > 0) {
801                messages.add(getString((mIsAlphaMode) ? R.string.lockpassword_password_recently_used
802                        : R.string.lockpassword_pin_recently_used));
803            }
804            return messages.toArray(new String[0]);
805        }
806
807        private int getMinLengthToFulfillAllPolicies() {
808            final int minLengthForLetters = Math.max(mPasswordMinLetters,
809                    mPasswordMinUpperCase + mPasswordMinLowerCase);
810            final int minLengthForNonLetters = Math.max(mPasswordMinNonLetter,
811                    mPasswordMinSymbols + mPasswordMinNumeric);
812            return minLengthForLetters + minLengthForNonLetters;
813        }
814
815        /**
816         * Update the hint based on current Stage and length of password entry
817         */
818        private void updateUi() {
819            final boolean canInput = mSaveAndFinishWorker == null;
820            String password = mPasswordEntry.getText().toString();
821            final int length = password.length();
822            if (mUiStage == Stage.Introduction) {
823                mPasswordRestrictionView.setVisibility(View.VISIBLE);
824                final int errorCode = validatePassword(password);
825                String[] messages = convertErrorCodeToMessages(errorCode);
826                // Update the fulfillment of requirements.
827                mPasswordRequirementAdapter.setRequirements(messages);
828                // Enable/Disable the next button accordingly.
829                setNextEnabled(errorCode == NO_ERROR);
830            } else {
831                // Hide password requirement view when we are just asking user to confirm the pw.
832                mPasswordRestrictionView.setVisibility(View.GONE);
833                setHeaderText(getString(mUiStage.getHint(mIsAlphaMode, mForFingerprint)));
834                setNextEnabled(canInput && length > 0);
835            }
836            setNextText(mUiStage.buttonText);
837            mPasswordEntryInputDisabler.setInputEnabled(canInput);
838        }
839
840        private void setHeaderText(String text) {
841            // Only set the text if it is different than the existing one to avoid announcing again.
842            if (!TextUtils.isEmpty(mLayout.getHeaderText())
843                    && mLayout.getHeaderText().toString().equals(text)) {
844                return;
845            }
846            mLayout.setHeaderText(text);
847        }
848
849        public void afterTextChanged(Editable s) {
850            // Changing the text while error displayed resets to NeedToConfirm state
851            if (mUiStage == Stage.ConfirmWrong) {
852                mUiStage = Stage.NeedToConfirm;
853            }
854            // Schedule the UI update.
855            mTextChangedHandler.notifyAfterTextChanged();
856        }
857
858        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
859
860        }
861
862        public void onTextChanged(CharSequence s, int start, int before, int count) {
863
864        }
865
866        private void startSaveAndFinish() {
867            if (mSaveAndFinishWorker != null) {
868                Log.w(TAG, "startSaveAndFinish with an existing SaveAndFinishWorker.");
869                return;
870            }
871
872            mPasswordEntryInputDisabler.setInputEnabled(false);
873            setNextEnabled(false);
874
875            mSaveAndFinishWorker = new SaveAndFinishWorker();
876            mSaveAndFinishWorker.setListener(this);
877
878            getFragmentManager().beginTransaction().add(mSaveAndFinishWorker,
879                    FRAGMENT_TAG_SAVE_AND_FINISH).commit();
880            getFragmentManager().executePendingTransactions();
881
882            final boolean required = getActivity().getIntent().getBooleanExtra(
883                    EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true);
884            mSaveAndFinishWorker.start(mLockPatternUtils, required, mHasChallenge, mChallenge,
885                    mChosenPassword, mCurrentPassword, mRequestedQuality, mUserId);
886        }
887
888        @Override
889        public void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData) {
890            getActivity().setResult(RESULT_FINISHED, resultData);
891
892            if (!wasSecureBefore) {
893                Intent intent = getRedactionInterstitialIntent(getActivity());
894                if (intent != null) {
895                    intent.putExtra(EXTRA_HIDE_DRAWER, mHideDrawer);
896                    startActivity(intent);
897                }
898            }
899            getActivity().finish();
900        }
901
902        class TextChangedHandler extends Handler {
903            private static final int ON_TEXT_CHANGED = 1;
904            private static final int DELAY_IN_MILLISECOND = 100;
905
906            /**
907             * With the introduction of delay, we batch processing the text changed event to reduce
908             * unnecessary UI updates.
909             */
910            private void notifyAfterTextChanged() {
911                removeMessages(ON_TEXT_CHANGED);
912                sendEmptyMessageDelayed(ON_TEXT_CHANGED, DELAY_IN_MILLISECOND);
913            }
914
915            @Override
916            public void handleMessage(Message msg) {
917                if (getActivity() == null) {
918                    return;
919                }
920                if (msg.what == ON_TEXT_CHANGED) {
921                    updateUi();
922                }
923            }
924        }
925    }
926
927    public static class SaveAndFinishWorker extends SaveChosenLockWorkerBase {
928
929        private String mChosenPassword;
930        private String mCurrentPassword;
931        private int mRequestedQuality;
932
933        public void start(LockPatternUtils utils, boolean required,
934                boolean hasChallenge, long challenge,
935                String chosenPassword, String currentPassword, int requestedQuality, int userId) {
936            prepare(utils, required, hasChallenge, challenge, userId);
937
938            mChosenPassword = chosenPassword;
939            mCurrentPassword = currentPassword;
940            mRequestedQuality = requestedQuality;
941            mUserId = userId;
942
943            start();
944        }
945
946        @Override
947        protected Intent saveAndVerifyInBackground() {
948            Intent result = null;
949            mUtils.saveLockPassword(mChosenPassword, mCurrentPassword, mRequestedQuality,
950                    mUserId);
951
952            if (mHasChallenge) {
953                byte[] token;
954                try {
955                    token = mUtils.verifyPassword(mChosenPassword, mChallenge, mUserId);
956                } catch (RequestThrottledException e) {
957                    token = null;
958                }
959
960                if (token == null) {
961                    Log.e(TAG, "critical: no token returned for known good password.");
962                }
963
964                result = new Intent();
965                result.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token);
966            }
967
968            return result;
969        }
970    }
971}
972