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