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