ChooseLockPassword.java revision ecd2b7b81fd2faa2f2f3dbe5a169c749321f3d89
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;
18
19import com.android.internal.widget.LockPatternUtils;
20import com.android.internal.widget.PasswordEntryKeyboardHelper;
21import com.android.internal.widget.PasswordEntryKeyboardView;
22import com.android.settings.notification.RedactionInterstitial;
23
24import android.app.Activity;
25import android.app.Fragment;
26import android.app.admin.DevicePolicyManager;
27import android.content.ContentResolver;
28import android.content.Context;
29import android.content.Intent;
30import android.inputmethodservice.KeyboardView;
31import android.os.Bundle;
32import android.os.Handler;
33import android.os.Message;
34import android.provider.Settings;
35import android.text.Editable;
36import android.text.InputType;
37import android.text.Selection;
38import android.text.Spannable;
39import android.text.TextUtils;
40import android.text.TextWatcher;
41import android.view.KeyEvent;
42import android.view.LayoutInflater;
43import android.view.View;
44import android.view.ViewGroup;
45import android.view.View.OnClickListener;
46import android.view.inputmethod.EditorInfo;
47import android.widget.Button;
48import android.widget.TextView;
49import android.widget.TextView.OnEditorActionListener;
50
51public class ChooseLockPassword extends SettingsActivity {
52    public static final String PASSWORD_MIN_KEY = "lockscreen.password_min";
53    public static final String PASSWORD_MAX_KEY = "lockscreen.password_max";
54    public static final String PASSWORD_MIN_LETTERS_KEY = "lockscreen.password_min_letters";
55    public static final String PASSWORD_MIN_LOWERCASE_KEY = "lockscreen.password_min_lowercase";
56    public static final String PASSWORD_MIN_UPPERCASE_KEY = "lockscreen.password_min_uppercase";
57    public static final String PASSWORD_MIN_NUMERIC_KEY = "lockscreen.password_min_numeric";
58    public static final String PASSWORD_MIN_SYMBOLS_KEY = "lockscreen.password_min_symbols";
59    public static final String PASSWORD_MIN_NONLETTER_KEY = "lockscreen.password_min_nonletter";
60
61    @Override
62    public Intent getIntent() {
63        Intent modIntent = new Intent(super.getIntent());
64        modIntent.putExtra(EXTRA_SHOW_FRAGMENT, getFragmentClass().getName());
65        return modIntent;
66    }
67
68    public static Intent createIntent(Context context, int quality, final boolean isFallback,
69            int minLength, final int maxLength, boolean requirePasswordToDecrypt,
70            boolean confirmCredentials) {
71        Intent intent = new Intent().setClass(context, ChooseLockPassword.class);
72        intent.putExtra(LockPatternUtils.PASSWORD_TYPE_KEY, quality);
73        intent.putExtra(PASSWORD_MIN_KEY, minLength);
74        intent.putExtra(PASSWORD_MAX_KEY, maxLength);
75        intent.putExtra(ChooseLockGeneric.CONFIRM_CREDENTIALS, confirmCredentials);
76        intent.putExtra(LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK, isFallback);
77        intent.putExtra(EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, requirePasswordToDecrypt);
78        return intent;
79    }
80
81    @Override
82    protected boolean isValidFragment(String fragmentName) {
83        if (ChooseLockPasswordFragment.class.getName().equals(fragmentName)) return true;
84        return false;
85    }
86
87    /* package */ Class<? extends Fragment> getFragmentClass() {
88        return ChooseLockPasswordFragment.class;
89    }
90
91    @Override
92    public void onCreate(Bundle savedInstanceState) {
93        // TODO: Fix on phones
94        // Disable IME on our window since we provide our own keyboard
95        //getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
96                //WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
97        super.onCreate(savedInstanceState);
98        CharSequence msg = getText(R.string.lockpassword_choose_your_password_header);
99        setTitle(msg);
100    }
101
102    public static class ChooseLockPasswordFragment extends Fragment
103            implements OnClickListener, OnEditorActionListener,  TextWatcher {
104        private static final String KEY_FIRST_PIN = "first_pin";
105        private static final String KEY_UI_STAGE = "ui_stage";
106        private TextView mPasswordEntry;
107        private int mPasswordMinLength = 4;
108        private int mPasswordMaxLength = 16;
109        private int mPasswordMinLetters = 0;
110        private int mPasswordMinUpperCase = 0;
111        private int mPasswordMinLowerCase = 0;
112        private int mPasswordMinSymbols = 0;
113        private int mPasswordMinNumeric = 0;
114        private int mPasswordMinNonLetter = 0;
115        private LockPatternUtils mLockPatternUtils;
116        private int mRequestedQuality = DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
117        private ChooseLockSettingsHelper mChooseLockSettingsHelper;
118        private Stage mUiStage = Stage.Introduction;
119        private boolean mDone = false;
120        private TextView mHeaderText;
121        private String mFirstPin;
122        private KeyboardView mKeyboardView;
123        private PasswordEntryKeyboardHelper mKeyboardHelper;
124        private boolean mIsAlphaMode;
125        private Button mCancelButton;
126        private Button mNextButton;
127        private static final int CONFIRM_EXISTING_REQUEST = 58;
128        static final int RESULT_FINISHED = RESULT_FIRST_USER;
129        private static final long ERROR_MESSAGE_TIMEOUT = 3000;
130        private static final int MSG_SHOW_ERROR = 1;
131
132        private Handler mHandler = new Handler() {
133            @Override
134            public void handleMessage(Message msg) {
135                if (msg.what == MSG_SHOW_ERROR) {
136                    updateStage((Stage) msg.obj);
137                }
138            }
139        };
140
141        /**
142         * Keep track internally of where the user is in choosing a pattern.
143         */
144        protected enum Stage {
145
146            Introduction(R.string.lockpassword_choose_your_password_header,
147                    R.string.lockpassword_choose_your_pin_header,
148                    R.string.lockpassword_continue_label),
149
150            NeedToConfirm(R.string.lockpassword_confirm_your_password_header,
151                    R.string.lockpassword_confirm_your_pin_header,
152                    R.string.lockpassword_ok_label),
153
154            ConfirmWrong(R.string.lockpassword_confirm_passwords_dont_match,
155                    R.string.lockpassword_confirm_pins_dont_match,
156                    R.string.lockpassword_continue_label);
157
158            Stage(int hintInAlpha, int hintInNumeric, int nextButtonText) {
159                this.alphaHint = hintInAlpha;
160                this.numericHint = hintInNumeric;
161                this.buttonText = nextButtonText;
162            }
163
164            public final int alphaHint;
165            public final int numericHint;
166            public final int buttonText;
167        }
168
169        // required constructor for fragments
170        public ChooseLockPasswordFragment() {
171
172        }
173
174        @Override
175        public void onCreate(Bundle savedInstanceState) {
176            super.onCreate(savedInstanceState);
177            mLockPatternUtils = new LockPatternUtils(getActivity());
178            Intent intent = getActivity().getIntent();
179            if (!(getActivity() instanceof ChooseLockPassword)) {
180                throw new SecurityException("Fragment contained in wrong activity");
181            }
182            mRequestedQuality = Math.max(intent.getIntExtra(LockPatternUtils.PASSWORD_TYPE_KEY,
183                    mRequestedQuality), mLockPatternUtils.getRequestedPasswordQuality());
184            mPasswordMinLength = Math.max(
185                    intent.getIntExtra(PASSWORD_MIN_KEY, mPasswordMinLength), mLockPatternUtils
186                            .getRequestedMinimumPasswordLength());
187            mPasswordMaxLength = intent.getIntExtra(PASSWORD_MAX_KEY, mPasswordMaxLength);
188            mPasswordMinLetters = Math.max(intent.getIntExtra(PASSWORD_MIN_LETTERS_KEY,
189                    mPasswordMinLetters), mLockPatternUtils.getRequestedPasswordMinimumLetters());
190            mPasswordMinUpperCase = Math.max(intent.getIntExtra(PASSWORD_MIN_UPPERCASE_KEY,
191                    mPasswordMinUpperCase), mLockPatternUtils.getRequestedPasswordMinimumUpperCase());
192            mPasswordMinLowerCase = Math.max(intent.getIntExtra(PASSWORD_MIN_LOWERCASE_KEY,
193                    mPasswordMinLowerCase), mLockPatternUtils.getRequestedPasswordMinimumLowerCase());
194            mPasswordMinNumeric = Math.max(intent.getIntExtra(PASSWORD_MIN_NUMERIC_KEY,
195                    mPasswordMinNumeric), mLockPatternUtils.getRequestedPasswordMinimumNumeric());
196            mPasswordMinSymbols = Math.max(intent.getIntExtra(PASSWORD_MIN_SYMBOLS_KEY,
197                    mPasswordMinSymbols), mLockPatternUtils.getRequestedPasswordMinimumSymbols());
198            mPasswordMinNonLetter = Math.max(intent.getIntExtra(PASSWORD_MIN_NONLETTER_KEY,
199                    mPasswordMinNonLetter), mLockPatternUtils.getRequestedPasswordMinimumNonLetter());
200
201            mChooseLockSettingsHelper = new ChooseLockSettingsHelper(getActivity());
202        }
203
204        @Override
205        public View onCreateView(LayoutInflater inflater, ViewGroup container,
206                Bundle savedInstanceState) {
207            return inflater.inflate(R.layout.choose_lock_password, container, false);
208        }
209
210        @Override
211        public void onViewCreated(View view, Bundle savedInstanceState) {
212            super.onViewCreated(view, savedInstanceState);
213
214            mCancelButton = (Button) view.findViewById(R.id.cancel_button);
215            mCancelButton.setOnClickListener(this);
216            mNextButton = (Button) view.findViewById(R.id.next_button);
217            mNextButton.setOnClickListener(this);
218
219            mIsAlphaMode = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == mRequestedQuality
220                    || DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == mRequestedQuality
221                    || DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == mRequestedQuality;
222            mKeyboardView = (PasswordEntryKeyboardView) view.findViewById(R.id.keyboard);
223            mPasswordEntry = (TextView) view.findViewById(R.id.password_entry);
224            mPasswordEntry.setOnEditorActionListener(this);
225            mPasswordEntry.addTextChangedListener(this);
226
227            final Activity activity = getActivity();
228            mKeyboardHelper = new PasswordEntryKeyboardHelper(activity,
229                    mKeyboardView, mPasswordEntry);
230            mKeyboardHelper.setKeyboardMode(mIsAlphaMode ?
231                    PasswordEntryKeyboardHelper.KEYBOARD_MODE_ALPHA
232                    : PasswordEntryKeyboardHelper.KEYBOARD_MODE_NUMERIC);
233
234            mHeaderText = (TextView) view.findViewById(R.id.headerText);
235            mKeyboardView.requestFocus();
236
237            int currentType = mPasswordEntry.getInputType();
238            mPasswordEntry.setInputType(mIsAlphaMode ? currentType
239                    : (InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD));
240
241            Intent intent = getActivity().getIntent();
242            final boolean confirmCredentials = intent.getBooleanExtra("confirm_credentials", true);
243            if (savedInstanceState == null) {
244                updateStage(Stage.Introduction);
245                if (confirmCredentials) {
246                    mChooseLockSettingsHelper.launchConfirmationActivity(CONFIRM_EXISTING_REQUEST,
247                            null, null);
248                }
249            } else {
250                mFirstPin = savedInstanceState.getString(KEY_FIRST_PIN);
251                final String state = savedInstanceState.getString(KEY_UI_STAGE);
252                if (state != null) {
253                    mUiStage = Stage.valueOf(state);
254                    updateStage(mUiStage);
255                }
256            }
257            mDone = false;
258            if (activity instanceof SettingsActivity) {
259                final SettingsActivity sa = (SettingsActivity) activity;
260                int id = mIsAlphaMode ? R.string.lockpassword_choose_your_password_header
261                        : R.string.lockpassword_choose_your_pin_header;
262                CharSequence title = getText(id);
263                sa.setTitle(title);
264            }
265        }
266
267        @Override
268        public void onResume() {
269            super.onResume();
270            updateStage(mUiStage);
271            mKeyboardView.requestFocus();
272        }
273
274        @Override
275        public void onPause() {
276            mHandler.removeMessages(MSG_SHOW_ERROR);
277
278            super.onPause();
279        }
280
281        @Override
282        public void onSaveInstanceState(Bundle outState) {
283            super.onSaveInstanceState(outState);
284            outState.putString(KEY_UI_STAGE, mUiStage.name());
285            outState.putString(KEY_FIRST_PIN, mFirstPin);
286        }
287
288        @Override
289        public void onActivityResult(int requestCode, int resultCode,
290                Intent data) {
291            super.onActivityResult(requestCode, resultCode, data);
292            switch (requestCode) {
293                case CONFIRM_EXISTING_REQUEST:
294                    if (resultCode != Activity.RESULT_OK) {
295                        getActivity().setResult(RESULT_FINISHED);
296                        getActivity().finish();
297                    }
298                    break;
299            }
300        }
301
302        protected Intent getRedactionInterstitialIntent(Context context) {
303            return RedactionInterstitial.createStartIntent(context);
304        }
305
306        protected void updateStage(Stage stage) {
307            final Stage previousStage = mUiStage;
308            mUiStage = stage;
309            updateUi();
310
311            // If the stage changed, announce the header for accessibility. This
312            // is a no-op when accessibility is disabled.
313            if (previousStage != stage) {
314                mHeaderText.announceForAccessibility(mHeaderText.getText());
315            }
316        }
317
318        /**
319         * Validates PIN and returns a message to display if PIN fails test.
320         * @param password the raw password the user typed in
321         * @return error message to show to user or null if password is OK
322         */
323        private String validatePassword(String password) {
324            if (password.length() < mPasswordMinLength) {
325                return getString(mIsAlphaMode ?
326                        R.string.lockpassword_password_too_short
327                        : R.string.lockpassword_pin_too_short, mPasswordMinLength);
328            }
329            if (password.length() > mPasswordMaxLength) {
330                return getString(mIsAlphaMode ?
331                        R.string.lockpassword_password_too_long
332                        : R.string.lockpassword_pin_too_long, mPasswordMaxLength + 1);
333            }
334            int letters = 0;
335            int numbers = 0;
336            int lowercase = 0;
337            int symbols = 0;
338            int uppercase = 0;
339            int nonletter = 0;
340            for (int i = 0; i < password.length(); i++) {
341                char c = password.charAt(i);
342                // allow non control Latin-1 characters only
343                if (c < 32 || c > 127) {
344                    return getString(R.string.lockpassword_illegal_character);
345                }
346                if (c >= '0' && c <= '9') {
347                    numbers++;
348                    nonletter++;
349                } else if (c >= 'A' && c <= 'Z') {
350                    letters++;
351                    uppercase++;
352                } else if (c >= 'a' && c <= 'z') {
353                    letters++;
354                    lowercase++;
355                } else {
356                    symbols++;
357                    nonletter++;
358                }
359            }
360            if (DevicePolicyManager.PASSWORD_QUALITY_NUMERIC == mRequestedQuality
361                    || DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX == mRequestedQuality) {
362                if (letters > 0 || symbols > 0) {
363                    // This shouldn't be possible unless user finds some way to bring up
364                    // soft keyboard
365                    return getString(R.string.lockpassword_pin_contains_non_digits);
366                }
367                // Check for repeated characters or sequences (e.g. '1234', '0000', '2468')
368                final int sequence = LockPatternUtils.maxLengthSequence(password);
369                if (DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX == mRequestedQuality
370                        && sequence > LockPatternUtils.MAX_ALLOWED_SEQUENCE) {
371                    return getString(R.string.lockpassword_pin_no_sequential_digits);
372                }
373            } else if (DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == mRequestedQuality) {
374                if (letters < mPasswordMinLetters) {
375                    return String.format(getResources().getQuantityString(
376                            R.plurals.lockpassword_password_requires_letters, mPasswordMinLetters),
377                            mPasswordMinLetters);
378                } else if (numbers < mPasswordMinNumeric) {
379                    return String.format(getResources().getQuantityString(
380                            R.plurals.lockpassword_password_requires_numeric, mPasswordMinNumeric),
381                            mPasswordMinNumeric);
382                } else if (lowercase < mPasswordMinLowerCase) {
383                    return String.format(getResources().getQuantityString(
384                            R.plurals.lockpassword_password_requires_lowercase, mPasswordMinLowerCase),
385                            mPasswordMinLowerCase);
386                } else if (uppercase < mPasswordMinUpperCase) {
387                    return String.format(getResources().getQuantityString(
388                            R.plurals.lockpassword_password_requires_uppercase, mPasswordMinUpperCase),
389                            mPasswordMinUpperCase);
390                } else if (symbols < mPasswordMinSymbols) {
391                    return String.format(getResources().getQuantityString(
392                            R.plurals.lockpassword_password_requires_symbols, mPasswordMinSymbols),
393                            mPasswordMinSymbols);
394                } else if (nonletter < mPasswordMinNonLetter) {
395                    return String.format(getResources().getQuantityString(
396                            R.plurals.lockpassword_password_requires_nonletter, mPasswordMinNonLetter),
397                            mPasswordMinNonLetter);
398                }
399            } else {
400                final boolean alphabetic = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
401                        == mRequestedQuality;
402                final boolean alphanumeric = DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC
403                        == mRequestedQuality;
404                if ((alphabetic || alphanumeric) && letters == 0) {
405                    return getString(R.string.lockpassword_password_requires_alpha);
406                }
407                if (alphanumeric && numbers == 0) {
408                    return getString(R.string.lockpassword_password_requires_digit);
409                }
410            }
411            if(mLockPatternUtils.checkPasswordHistory(password)) {
412                return getString(mIsAlphaMode ? R.string.lockpassword_password_recently_used
413                        : R.string.lockpassword_pin_recently_used);
414            }
415
416            return null;
417        }
418
419        private void handleNext() {
420            if (mDone) return;
421
422            final String pin = mPasswordEntry.getText().toString();
423            if (TextUtils.isEmpty(pin)) {
424                return;
425            }
426            String errorMsg = null;
427            if (mUiStage == Stage.Introduction) {
428                errorMsg = validatePassword(pin);
429                if (errorMsg == null) {
430                    mFirstPin = pin;
431                    mPasswordEntry.setText("");
432                    updateStage(Stage.NeedToConfirm);
433                }
434            } else if (mUiStage == Stage.NeedToConfirm) {
435                if (mFirstPin.equals(pin)) {
436                    final boolean isFallback = getActivity().getIntent().getBooleanExtra(
437                            LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK, false);
438                    boolean wasSecureBefore = mLockPatternUtils.isSecure();
439                    mLockPatternUtils.clearLock(isFallback);
440                    final boolean required = getActivity().getIntent().getBooleanExtra(
441                            EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true);
442                    mLockPatternUtils.setCredentialRequiredToDecrypt(required);
443                    mLockPatternUtils.saveLockPassword(pin, mRequestedQuality, isFallback);
444                    getActivity().setResult(RESULT_FINISHED);
445                    getActivity().finish();
446                    mDone = true;
447                    if (!wasSecureBefore) {
448                        startActivity(getRedactionInterstitialIntent(getActivity()));
449                    }
450                } else {
451                    CharSequence tmp = mPasswordEntry.getText();
452                    if (tmp != null) {
453                        Selection.setSelection((Spannable) tmp, 0, tmp.length());
454                    }
455                    updateStage(Stage.ConfirmWrong);
456                }
457            }
458            if (errorMsg != null) {
459                showError(errorMsg, mUiStage);
460            }
461        }
462
463        public void onClick(View v) {
464            switch (v.getId()) {
465                case R.id.next_button:
466                    handleNext();
467                    break;
468
469                case R.id.cancel_button:
470                    getActivity().finish();
471                    break;
472            }
473        }
474
475        private void showError(String msg, final Stage next) {
476            mHeaderText.setText(msg);
477            mHeaderText.announceForAccessibility(mHeaderText.getText());
478            Message mesg = mHandler.obtainMessage(MSG_SHOW_ERROR, next);
479            mHandler.removeMessages(MSG_SHOW_ERROR);
480            mHandler.sendMessageDelayed(mesg, ERROR_MESSAGE_TIMEOUT);
481        }
482
483        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
484            // Check if this was the result of hitting the enter or "done" key
485            if (actionId == EditorInfo.IME_NULL
486                    || actionId == EditorInfo.IME_ACTION_DONE
487                    || actionId == EditorInfo.IME_ACTION_NEXT) {
488                handleNext();
489                return true;
490            }
491            return false;
492        }
493
494        /**
495         * Update the hint based on current Stage and length of password entry
496         */
497        private void updateUi() {
498            String password = mPasswordEntry.getText().toString();
499            final int length = password.length();
500            if (mUiStage == Stage.Introduction && length > 0) {
501                if (length < mPasswordMinLength) {
502                    String msg = getString(mIsAlphaMode ? R.string.lockpassword_password_too_short
503                            : R.string.lockpassword_pin_too_short, mPasswordMinLength);
504                    mHeaderText.setText(msg);
505                    mNextButton.setEnabled(false);
506                } else {
507                    String error = validatePassword(password);
508                    if (error != null) {
509                        mHeaderText.setText(error);
510                        mNextButton.setEnabled(false);
511                    } else {
512                        mHeaderText.setText(R.string.lockpassword_press_continue);
513                        mNextButton.setEnabled(true);
514                    }
515                }
516            } else {
517                mHeaderText.setText(mIsAlphaMode ? mUiStage.alphaHint : mUiStage.numericHint);
518                mNextButton.setEnabled(length > 0);
519            }
520            mNextButton.setText(mUiStage.buttonText);
521        }
522
523        public void afterTextChanged(Editable s) {
524            // Changing the text while error displayed resets to NeedToConfirm state
525            if (mUiStage == Stage.ConfirmWrong) {
526                mUiStage = Stage.NeedToConfirm;
527            }
528            updateUi();
529        }
530
531        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
532
533        }
534
535        public void onTextChanged(CharSequence s, int start, int before, int count) {
536
537        }
538    }
539}
540