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