ChooseLockPassword.java revision 17e9e1933055b207473b488595b2887bfcaea32e
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;
22
23import android.app.Activity;
24import android.app.Fragment;
25import android.app.admin.DevicePolicyManager;
26import android.content.Intent;
27import android.inputmethodservice.KeyboardView;
28import android.os.Bundle;
29import android.os.Handler;
30import android.preference.PreferenceActivity;
31import android.text.Editable;
32import android.text.Selection;
33import android.text.Spannable;
34import android.text.TextUtils;
35import android.text.TextWatcher;
36import android.view.KeyEvent;
37import android.view.LayoutInflater;
38import android.view.View;
39import android.view.ViewGroup;
40import android.view.View.OnClickListener;
41import android.view.inputmethod.EditorInfo;
42import android.widget.Button;
43import android.widget.TextView;
44import android.widget.TextView.OnEditorActionListener;
45
46
47public class ChooseLockPassword extends PreferenceActivity {
48    public static final String PASSWORD_MIN_KEY = "lockscreen.password_min";
49    public static final String PASSWORD_MAX_KEY = "lockscreen.password_max";
50    public static final String PASSWORD_MIN_LETTERS_KEY = "lockscreen.password_min_letters";
51    public static final String PASSWORD_MIN_LOWERCASE_KEY = "lockscreen.password_min_lowercase";
52    public static final String PASSWORD_MIN_UPPERCASE_KEY = "lockscreen.password_min_uppercase";
53    public static final String PASSWORD_MIN_NUMERIC_KEY = "lockscreen.password_min_numeric";
54    public static final String PASSWORD_MIN_SYMBOLS_KEY = "lockscreen.password_min_symbols";
55    public static final String PASSWORD_MIN_NONLETTER_KEY = "lockscreen.password_min_nonletter";
56
57    @Override
58    public Intent getIntent() {
59        Intent modIntent = new Intent(super.getIntent());
60        modIntent.putExtra(EXTRA_SHOW_FRAGMENT, ChooseLockPasswordFragment.class.getName());
61        modIntent.putExtra(EXTRA_NO_HEADERS, true);
62        return modIntent;
63    }
64
65    @Override
66    public void onCreate(Bundle savedInstanceState) {
67        // TODO: Fix on phones
68        // Disable IME on our window since we provide our own keyboard
69        //getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
70                //WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
71        super.onCreate(savedInstanceState);
72    }
73
74    public static class ChooseLockPasswordFragment extends Fragment
75            implements OnClickListener, OnEditorActionListener,  TextWatcher {
76        private static final String KEY_FIRST_PIN = "first_pin";
77        private static final String KEY_UI_STAGE = "ui_stage";
78        private TextView mPasswordEntry;
79        private int mPasswordMinLength = 4;
80        private int mPasswordMaxLength = 16;
81        private int mPasswordMinLetters = 0;
82        private int mPasswordMinUpperCase = 0;
83        private int mPasswordMinLowerCase = 0;
84        private int mPasswordMinSymbols = 0;
85        private int mPasswordMinNumeric = 0;
86        private int mPasswordMinNonLetter = 0;
87        private LockPatternUtils mLockPatternUtils;
88        private int mRequestedQuality = DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
89        private ChooseLockSettingsHelper mChooseLockSettingsHelper;
90        private Stage mUiStage = Stage.Introduction;
91        private TextView mHeaderText;
92        private String mFirstPin;
93        private KeyboardView mKeyboardView;
94        private PasswordEntryKeyboardHelper mKeyboardHelper;
95        private boolean mIsAlphaMode;
96        private Button mCancelButton;
97        private Button mNextButton;
98        private static Handler mHandler = new Handler();
99        private static final int CONFIRM_EXISTING_REQUEST = 58;
100        static final int RESULT_FINISHED = RESULT_FIRST_USER;
101        private static final long ERROR_MESSAGE_TIMEOUT = 3000;
102
103        /**
104         * Keep track internally of where the user is in choosing a pattern.
105         */
106        protected enum Stage {
107
108            Introduction(R.string.lockpassword_choose_your_password_header,
109                    R.string.lockpassword_choose_your_pin_header,
110                    R.string.lockpassword_continue_label),
111
112            NeedToConfirm(R.string.lockpassword_confirm_your_password_header,
113                    R.string.lockpassword_confirm_your_pin_header,
114                    R.string.lockpassword_ok_label),
115
116            ConfirmWrong(R.string.lockpassword_confirm_passwords_dont_match,
117                    R.string.lockpassword_confirm_pins_dont_match,
118                    R.string.lockpassword_continue_label);
119
120            /**
121             * @param headerMessage The message displayed at the top.
122             */
123            Stage(int hintInAlpha, int hintInNumeric, int nextButtonText) {
124                this.alphaHint = hintInAlpha;
125                this.numericHint = hintInNumeric;
126                this.buttonText = nextButtonText;
127            }
128
129            public final int alphaHint;
130            public final int numericHint;
131            public final int buttonText;
132        }
133
134        // required constructor for fragments
135        public ChooseLockPasswordFragment() {
136
137        }
138
139        @Override
140        public void onCreate(Bundle savedInstanceState) {
141            super.onCreate(savedInstanceState);
142            mLockPatternUtils = new LockPatternUtils(getActivity());
143            Intent intent = getActivity().getIntent();
144            mRequestedQuality = Math.max(intent.getIntExtra(LockPatternUtils.PASSWORD_TYPE_KEY,
145                    mRequestedQuality), mLockPatternUtils.getRequestedPasswordQuality());
146            mPasswordMinLength = Math.max(
147                    intent.getIntExtra(PASSWORD_MIN_KEY, mPasswordMinLength), mLockPatternUtils
148                            .getRequestedMinimumPasswordLength());
149            mPasswordMaxLength = intent.getIntExtra(PASSWORD_MAX_KEY, mPasswordMaxLength);
150            mPasswordMinLetters = Math.max(intent.getIntExtra(PASSWORD_MIN_LETTERS_KEY,
151                    mPasswordMinLetters), mLockPatternUtils.getRequestedPasswordMinimumLetters());
152            mPasswordMinUpperCase = Math.max(intent.getIntExtra(PASSWORD_MIN_UPPERCASE_KEY,
153                    mPasswordMinUpperCase), mLockPatternUtils.getRequestedPasswordMinimumUpperCase());
154            mPasswordMinLowerCase = Math.max(intent.getIntExtra(PASSWORD_MIN_LOWERCASE_KEY,
155                    mPasswordMinLowerCase), mLockPatternUtils.getRequestedPasswordMinimumLowerCase());
156            mPasswordMinNumeric = Math.max(intent.getIntExtra(PASSWORD_MIN_NUMERIC_KEY,
157                    mPasswordMinNumeric), mLockPatternUtils.getRequestedPasswordMinimumNumeric());
158            mPasswordMinSymbols = Math.max(intent.getIntExtra(PASSWORD_MIN_SYMBOLS_KEY,
159                    mPasswordMinSymbols), mLockPatternUtils.getRequestedPasswordMinimumSymbols());
160            mPasswordMinNonLetter = Math.max(intent.getIntExtra(PASSWORD_MIN_NONLETTER_KEY,
161                    mPasswordMinNonLetter), mLockPatternUtils.getRequestedPasswordMinimumNonLetter());
162
163            mChooseLockSettingsHelper = new ChooseLockSettingsHelper(getActivity());
164        }
165
166        @Override
167        public View onCreateView(LayoutInflater inflater, ViewGroup container,
168                Bundle savedInstanceState) {
169
170            View view = inflater.inflate(R.layout.choose_lock_password, null);
171
172            mCancelButton = (Button) view.findViewById(R.id.cancel_button);
173            mCancelButton.setOnClickListener(this);
174            mNextButton = (Button) view.findViewById(R.id.next_button);
175            mNextButton.setOnClickListener(this);
176
177            mIsAlphaMode = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == mRequestedQuality
178                    || DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == mRequestedQuality
179                    || DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == mRequestedQuality;
180            mKeyboardView = (PasswordEntryKeyboardView) view.findViewById(R.id.keyboard);
181            mPasswordEntry = (TextView) view.findViewById(R.id.password_entry);
182            mPasswordEntry.setOnEditorActionListener(this);
183            mPasswordEntry.addTextChangedListener(this);
184
185            mKeyboardHelper = new PasswordEntryKeyboardHelper(getActivity(),
186                    mKeyboardView, mPasswordEntry);
187            mKeyboardHelper.setKeyboardMode(mIsAlphaMode ?
188                    PasswordEntryKeyboardHelper.KEYBOARD_MODE_ALPHA
189                    : PasswordEntryKeyboardHelper.KEYBOARD_MODE_NUMERIC);
190
191            mHeaderText = (TextView) view.findViewById(R.id.headerText);
192            mKeyboardView.requestFocus();
193
194            Intent intent = getActivity().getIntent();
195            final boolean confirmCredentials = intent.getBooleanExtra("confirm_credentials", true);
196            if (savedInstanceState == null) {
197                updateStage(Stage.Introduction);
198                if (confirmCredentials) {
199                    mChooseLockSettingsHelper.launchConfirmationActivity(CONFIRM_EXISTING_REQUEST,
200                            null, null);
201                }
202            } else {
203                mFirstPin = savedInstanceState.getString(KEY_FIRST_PIN);
204                final String state = savedInstanceState.getString(KEY_UI_STAGE);
205                if (state != null) {
206                    mUiStage = Stage.valueOf(state);
207                    updateStage(mUiStage);
208                }
209            }
210
211            return view;
212        }
213
214        @Override
215        public void onResume() {
216            super.onResume();
217            updateStage(mUiStage);
218            mKeyboardView.requestFocus();
219        }
220
221        @Override
222        public void onSaveInstanceState(Bundle outState) {
223            super.onSaveInstanceState(outState);
224            outState.putString(KEY_UI_STAGE, mUiStage.name());
225            outState.putString(KEY_FIRST_PIN, mFirstPin);
226        }
227
228        @Override
229        public void onActivityResult(int requestCode, int resultCode,
230                Intent data) {
231            super.onActivityResult(requestCode, resultCode, data);
232            switch (requestCode) {
233                case CONFIRM_EXISTING_REQUEST:
234                    if (resultCode != Activity.RESULT_OK) {
235                        getActivity().setResult(RESULT_FINISHED);
236                        getActivity().finish();
237                    }
238                    break;
239            }
240        }
241
242        protected void updateStage(Stage stage) {
243            mUiStage = stage;
244            updateUi();
245        }
246
247        /**
248         * Validates PIN and returns a message to display if PIN fails test.
249         * @param password the raw password the user typed in
250         * @return error message to show to user or null if password is OK
251         */
252        private String validatePassword(String password) {
253            if (password.length() < mPasswordMinLength) {
254                return getString(mIsAlphaMode ?
255                        R.string.lockpassword_password_too_short
256                        : R.string.lockpassword_pin_too_short, mPasswordMinLength);
257            }
258            if (password.length() > mPasswordMaxLength) {
259                return getString(mIsAlphaMode ?
260                        R.string.lockpassword_password_too_long
261                        : R.string.lockpassword_pin_too_long, mPasswordMaxLength);
262            }
263            int letters = 0;
264            int numbers = 0;
265            int lowercase = 0;
266            int symbols = 0;
267            int uppercase = 0;
268            int nonletter = 0;
269            for (int i = 0; i < password.length(); i++) {
270                char c = password.charAt(i);
271                // allow non white space Latin-1 characters only
272                if (c <= 32 || c > 127) {
273                    return getString(R.string.lockpassword_illegal_character);
274                }
275                if (c >= '0' && c <= '9') {
276                    numbers++;
277                    nonletter++;
278                } else if (c >= 'A' && c <= 'Z') {
279                    letters++;
280                    uppercase++;
281                } else if (c >= 'a' && c <= 'z') {
282                    letters++;
283                    lowercase++;
284                } else {
285                    symbols++;
286                    nonletter++;
287                }
288            }
289            if (DevicePolicyManager.PASSWORD_QUALITY_NUMERIC == mRequestedQuality
290                    && (letters > 0 || symbols > 0)) {
291                // This shouldn't be possible unless user finds some way to bring up
292                // soft keyboard
293                return getString(R.string.lockpassword_pin_contains_non_digits);
294            } else if (DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == mRequestedQuality) {
295                if (letters < mPasswordMinLetters) {
296                    return String.format(getResources().getQuantityString(
297                            R.plurals.lockpassword_password_requires_letters, mPasswordMinLetters),
298                            mPasswordMinLetters);
299                } else if (numbers < mPasswordMinNumeric) {
300                    return String.format(getResources().getQuantityString(
301                            R.plurals.lockpassword_password_requires_numeric, mPasswordMinNumeric),
302                            mPasswordMinNumeric);
303                } else if (lowercase < mPasswordMinLowerCase) {
304                    return String.format(getResources().getQuantityString(
305                            R.plurals.lockpassword_password_requires_lowercase, mPasswordMinLowerCase),
306                            mPasswordMinLowerCase);
307                } else if (uppercase < mPasswordMinUpperCase) {
308                    return String.format(getResources().getQuantityString(
309                            R.plurals.lockpassword_password_requires_uppercase, mPasswordMinUpperCase),
310                            mPasswordMinUpperCase);
311                } else if (symbols < mPasswordMinSymbols) {
312                    return String.format(getResources().getQuantityString(
313                            R.plurals.lockpassword_password_requires_symbols, mPasswordMinSymbols),
314                            mPasswordMinSymbols);
315                } else if (nonletter < mPasswordMinNonLetter) {
316                    return String.format(getResources().getQuantityString(
317                            R.plurals.lockpassword_password_requires_nonletter, mPasswordMinNonLetter),
318                            mPasswordMinNonLetter);
319                }
320            } else {
321                final boolean alphabetic = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
322                        == mRequestedQuality;
323                final boolean alphanumeric = DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC
324                        == mRequestedQuality;
325                if ((alphabetic || alphanumeric) && letters == 0) {
326                    return getString(R.string.lockpassword_password_requires_alpha);
327                }
328                if (alphanumeric && numbers == 0) {
329                    return getString(R.string.lockpassword_password_requires_digit);
330                }
331            }
332            if(mLockPatternUtils.checkPasswordHistory(password)) {
333                return getString(mIsAlphaMode ? R.string.lockpassword_password_recently_used
334                        : R.string.lockpassword_pin_recently_used);
335            }
336            return null;
337        }
338
339        private void handleNext() {
340            final String pin = mPasswordEntry.getText().toString();
341            if (TextUtils.isEmpty(pin)) {
342                return;
343            }
344            String errorMsg = null;
345            if (mUiStage == Stage.Introduction) {
346                errorMsg = validatePassword(pin);
347                if (errorMsg == null) {
348                    mFirstPin = pin;
349                    updateStage(Stage.NeedToConfirm);
350                    mPasswordEntry.setText("");
351                }
352            } else if (mUiStage == Stage.NeedToConfirm) {
353                if (mFirstPin.equals(pin)) {
354                    mLockPatternUtils.clearLock();
355                    mLockPatternUtils.saveLockPassword(pin, mRequestedQuality);
356                    getActivity().finish();
357                } else {
358                    updateStage(Stage.ConfirmWrong);
359                    CharSequence tmp = mPasswordEntry.getText();
360                    if (tmp != null) {
361                        Selection.setSelection((Spannable) tmp, 0, tmp.length());
362                    }
363                }
364            }
365            if (errorMsg != null) {
366                showError(errorMsg, mUiStage);
367            }
368        }
369
370        public void onClick(View v) {
371            switch (v.getId()) {
372                case R.id.next_button:
373                    handleNext();
374                    break;
375
376                case R.id.cancel_button:
377                    getActivity().finish();
378                    break;
379            }
380        }
381
382        private void showError(String msg, final Stage next) {
383            mHeaderText.setText(msg);
384            mHandler.postDelayed(new Runnable() {
385                public void run() {
386                    updateStage(next);
387                }
388            }, ERROR_MESSAGE_TIMEOUT);
389        }
390
391        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
392            // Check if this was the result of hitting the enter key
393            if (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_DOWN) {
394                handleNext();
395                return true;
396            }
397            return false;
398        }
399
400        /**
401         * Update the hint based on current Stage and length of password entry
402         */
403        private void updateUi() {
404            String password = mPasswordEntry.getText().toString();
405            final int length = password.length();
406            if (mUiStage == Stage.Introduction && length > 0) {
407                if (length < mPasswordMinLength) {
408                    String msg = getString(mIsAlphaMode ? R.string.lockpassword_password_too_short
409                            : R.string.lockpassword_pin_too_short, mPasswordMinLength);
410                    mHeaderText.setText(msg);
411                    mNextButton.setEnabled(false);
412                } else {
413                    String error = validatePassword(password);
414                    if (error != null) {
415                        mHeaderText.setText(error);
416                        mNextButton.setEnabled(false);
417                    } else {
418                        mHeaderText.setText(R.string.lockpassword_press_continue);
419                        mNextButton.setEnabled(true);
420                    }
421                }
422            } else {
423                mHeaderText.setText(mIsAlphaMode ? mUiStage.alphaHint : mUiStage.numericHint);
424                mNextButton.setEnabled(length > 0);
425            }
426            mNextButton.setText(mUiStage.buttonText);
427        }
428
429        public void afterTextChanged(Editable s) {
430            // Changing the text while error displayed resets to NeedToConfirm state
431            if (mUiStage == Stage.ConfirmWrong) {
432                mUiStage = Stage.NeedToConfirm;
433            }
434            updateUi();
435        }
436
437        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
438
439        }
440
441        public void onTextChanged(CharSequence s, int start, int before, int count) {
442
443        }
444    }
445}
446