Home | History | Annotate | Download | only in keyguard
      1 /*
      2  * Copyright (C) 2012 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.keyguard;
     18 
     19 import android.content.Context;
     20 import android.graphics.Rect;
     21 import android.text.Editable;
     22 import android.text.InputType;
     23 import android.text.TextUtils;
     24 import android.text.TextWatcher;
     25 import android.text.method.TextKeyListener;
     26 import android.util.AttributeSet;
     27 import android.view.KeyEvent;
     28 import android.view.View;
     29 import android.view.animation.AnimationUtils;
     30 import android.view.animation.Interpolator;
     31 import android.view.inputmethod.EditorInfo;
     32 import android.view.inputmethod.InputMethodInfo;
     33 import android.view.inputmethod.InputMethodManager;
     34 import android.view.inputmethod.InputMethodSubtype;
     35 import android.widget.TextView;
     36 import android.widget.TextView.OnEditorActionListener;
     37 
     38 import com.android.internal.widget.TextViewInputDisabler;
     39 
     40 import java.util.List;
     41 /**
     42  * Displays an alphanumeric (latin-1) key entry for the user to enter
     43  * an unlock password
     44  */
     45 public class KeyguardPasswordView extends KeyguardAbsKeyInputView
     46         implements KeyguardSecurityView, OnEditorActionListener, TextWatcher {
     47 
     48     private final boolean mShowImeAtScreenOn;
     49     private final int mDisappearYTranslation;
     50 
     51     // A delay constant to be used in a workaround for the situation where InputMethodManagerService
     52     // is not switched to the new user yet.
     53     // TODO: Remove this by ensuring such a race condition never happens.
     54     private static final int DELAY_MILLIS_TO_REEVALUATE_IME_SWITCH_ICON = 500;  // 500ms
     55 
     56     InputMethodManager mImm;
     57     private TextView mPasswordEntry;
     58     private TextViewInputDisabler mPasswordEntryDisabler;
     59     private View mSwitchImeButton;
     60 
     61     private Interpolator mLinearOutSlowInInterpolator;
     62     private Interpolator mFastOutLinearInInterpolator;
     63 
     64     public KeyguardPasswordView(Context context) {
     65         this(context, null);
     66     }
     67 
     68     public KeyguardPasswordView(Context context, AttributeSet attrs) {
     69         super(context, attrs);
     70         mShowImeAtScreenOn = context.getResources().
     71                 getBoolean(R.bool.kg_show_ime_at_screen_on);
     72         mDisappearYTranslation = getResources().getDimensionPixelSize(
     73                 R.dimen.disappear_y_translation);
     74         mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
     75                 context, android.R.interpolator.linear_out_slow_in);
     76         mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
     77                 context, android.R.interpolator.fast_out_linear_in);
     78     }
     79 
     80     @Override
     81     protected void resetState() {
     82         mSecurityMessageDisplay.setMessage("");
     83         final boolean wasDisabled = mPasswordEntry.isEnabled();
     84         setPasswordEntryEnabled(true);
     85         setPasswordEntryInputEnabled(true);
     86         if (wasDisabled) {
     87             mImm.showSoftInput(mPasswordEntry, InputMethodManager.SHOW_IMPLICIT);
     88         }
     89     }
     90 
     91     @Override
     92     protected int getPasswordTextViewId() {
     93         return R.id.passwordEntry;
     94     }
     95 
     96     @Override
     97     public boolean needsInput() {
     98         return true;
     99     }
    100 
    101     @Override
    102     public void onResume(final int reason) {
    103         super.onResume(reason);
    104 
    105         // Wait a bit to focus the field so the focusable flag on the window is already set then.
    106         post(new Runnable() {
    107             @Override
    108             public void run() {
    109                 if (isShown() && mPasswordEntry.isEnabled()) {
    110                     mPasswordEntry.requestFocus();
    111                     if (reason != KeyguardSecurityView.SCREEN_ON || mShowImeAtScreenOn) {
    112                         mImm.showSoftInput(mPasswordEntry, InputMethodManager.SHOW_IMPLICIT);
    113                     }
    114                 }
    115             }
    116         });
    117     }
    118 
    119     @Override
    120     protected int getPromtReasonStringRes(int reason) {
    121         switch (reason) {
    122             case PROMPT_REASON_RESTART:
    123                 return R.string.kg_prompt_reason_restart_password;
    124             case PROMPT_REASON_TIMEOUT:
    125                 return R.string.kg_prompt_reason_timeout_password;
    126             case PROMPT_REASON_DEVICE_ADMIN:
    127                 return R.string.kg_prompt_reason_device_admin;
    128             case PROMPT_REASON_USER_REQUEST:
    129                 return R.string.kg_prompt_reason_user_request;
    130             case PROMPT_REASON_NONE:
    131                 return 0;
    132             default:
    133                 return R.string.kg_prompt_reason_timeout_password;
    134         }
    135     }
    136 
    137     @Override
    138     public void onPause() {
    139         super.onPause();
    140         mImm.hideSoftInputFromWindow(getWindowToken(), 0);
    141     }
    142 
    143     @Override
    144     public void reset() {
    145         super.reset();
    146         mPasswordEntry.requestFocus();
    147     }
    148 
    149     private void updateSwitchImeButton() {
    150         // If there's more than one IME, enable the IME switcher button
    151         final boolean wasVisible = mSwitchImeButton.getVisibility() == View.VISIBLE;
    152         final boolean shouldBeVisible = hasMultipleEnabledIMEsOrSubtypes(mImm, false);
    153         if (wasVisible != shouldBeVisible) {
    154             mSwitchImeButton.setVisibility(shouldBeVisible ? View.VISIBLE : View.GONE);
    155         }
    156 
    157         // TODO: Check if we still need this hack.
    158         // If no icon is visible, reset the start margin on the password field so the text is
    159         // still centered.
    160         if (mSwitchImeButton.getVisibility() != View.VISIBLE) {
    161             android.view.ViewGroup.LayoutParams params = mPasswordEntry.getLayoutParams();
    162             if (params instanceof MarginLayoutParams) {
    163                 final MarginLayoutParams mlp = (MarginLayoutParams) params;
    164                 mlp.setMarginStart(0);
    165                 mPasswordEntry.setLayoutParams(params);
    166             }
    167         }
    168     }
    169 
    170     @Override
    171     protected void onFinishInflate() {
    172         super.onFinishInflate();
    173 
    174         mImm = (InputMethodManager) getContext().getSystemService(
    175                 Context.INPUT_METHOD_SERVICE);
    176 
    177         mPasswordEntry = findViewById(getPasswordTextViewId());
    178         mPasswordEntryDisabler = new TextViewInputDisabler(mPasswordEntry);
    179         mPasswordEntry.setKeyListener(TextKeyListener.getInstance());
    180         mPasswordEntry.setInputType(InputType.TYPE_CLASS_TEXT
    181                 | InputType.TYPE_TEXT_VARIATION_PASSWORD);
    182         mPasswordEntry.setOnEditorActionListener(this);
    183         mPasswordEntry.addTextChangedListener(this);
    184 
    185         // Poke the wakelock any time the text is selected or modified
    186         mPasswordEntry.setOnClickListener(new OnClickListener() {
    187             @Override
    188             public void onClick(View v) {
    189                 mCallback.userActivity();
    190             }
    191         });
    192 
    193         // Set selected property on so the view can send accessibility events.
    194         mPasswordEntry.setSelected(true);
    195 
    196         mPasswordEntry.requestFocus();
    197 
    198         mSwitchImeButton = findViewById(R.id.switch_ime_button);
    199         mSwitchImeButton.setOnClickListener(new OnClickListener() {
    200             @Override
    201             public void onClick(View v) {
    202                 mCallback.userActivity(); // Leave the screen on a bit longer
    203                 // Do not show auxiliary subtypes in password lock screen.
    204                 mImm.showInputMethodPicker(false /* showAuxiliarySubtypes */);
    205             }
    206         });
    207 
    208         // If there's more than one IME, enable the IME switcher button
    209         updateSwitchImeButton();
    210 
    211         // When we the current user is switching, InputMethodManagerService sometimes has not
    212         // switched internal state yet here. As a quick workaround, we check the keyboard state
    213         // again.
    214         // TODO: Remove this workaround by ensuring such a race condition never happens.
    215         postDelayed(new Runnable() {
    216             @Override
    217             public void run() {
    218                 updateSwitchImeButton();
    219             }
    220         }, DELAY_MILLIS_TO_REEVALUATE_IME_SWITCH_ICON);
    221     }
    222 
    223     @Override
    224     protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
    225         // send focus to the password field
    226         return mPasswordEntry.requestFocus(direction, previouslyFocusedRect);
    227     }
    228 
    229     @Override
    230     protected void resetPasswordText(boolean animate, boolean announce) {
    231         mPasswordEntry.setText("");
    232     }
    233 
    234     @Override
    235     protected String getPasswordText() {
    236         return mPasswordEntry.getText().toString();
    237     }
    238 
    239     @Override
    240     protected void setPasswordEntryEnabled(boolean enabled) {
    241         mPasswordEntry.setEnabled(enabled);
    242     }
    243 
    244     @Override
    245     protected void setPasswordEntryInputEnabled(boolean enabled) {
    246         mPasswordEntryDisabler.setInputEnabled(enabled);
    247     }
    248 
    249     /**
    250      * Method adapted from com.android.inputmethod.latin.Utils
    251      *
    252      * @param imm The input method manager
    253      * @param shouldIncludeAuxiliarySubtypes
    254      * @return true if we have multiple IMEs to choose from
    255      */
    256     private boolean hasMultipleEnabledIMEsOrSubtypes(InputMethodManager imm,
    257             final boolean shouldIncludeAuxiliarySubtypes) {
    258         final List<InputMethodInfo> enabledImis = imm.getEnabledInputMethodList();
    259 
    260         // Number of the filtered IMEs
    261         int filteredImisCount = 0;
    262 
    263         for (InputMethodInfo imi : enabledImis) {
    264             // We can return true immediately after we find two or more filtered IMEs.
    265             if (filteredImisCount > 1) return true;
    266             final List<InputMethodSubtype> subtypes =
    267                     imm.getEnabledInputMethodSubtypeList(imi, true);
    268             // IMEs that have no subtypes should be counted.
    269             if (subtypes.isEmpty()) {
    270                 ++filteredImisCount;
    271                 continue;
    272             }
    273 
    274             int auxCount = 0;
    275             for (InputMethodSubtype subtype : subtypes) {
    276                 if (subtype.isAuxiliary()) {
    277                     ++auxCount;
    278                 }
    279             }
    280             final int nonAuxCount = subtypes.size() - auxCount;
    281 
    282             // IMEs that have one or more non-auxiliary subtypes should be counted.
    283             // If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary
    284             // subtypes should be counted as well.
    285             if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) {
    286                 ++filteredImisCount;
    287                 continue;
    288             }
    289         }
    290 
    291         return filteredImisCount > 1
    292         // imm.getEnabledInputMethodSubtypeList(null, false) will return the current IME's enabled
    293         // input method subtype (The current IME should be LatinIME.)
    294                 || imm.getEnabledInputMethodSubtypeList(null, false).size() > 1;
    295     }
    296 
    297     @Override
    298     public void showUsabilityHint() {
    299     }
    300 
    301     @Override
    302     public int getWrongPasswordStringId() {
    303         return R.string.kg_wrong_password;
    304     }
    305 
    306     @Override
    307     public void startAppearAnimation() {
    308         setAlpha(0f);
    309         setTranslationY(0f);
    310         animate()
    311                 .alpha(1)
    312                 .withLayer()
    313                 .setDuration(300)
    314                 .setInterpolator(mLinearOutSlowInInterpolator);
    315     }
    316 
    317     @Override
    318     public boolean startDisappearAnimation(Runnable finishRunnable) {
    319         animate()
    320                 .alpha(0f)
    321                 .translationY(mDisappearYTranslation)
    322                 .setInterpolator(mFastOutLinearInInterpolator)
    323                 .setDuration(100)
    324                 .withEndAction(finishRunnable);
    325         return true;
    326     }
    327 
    328     @Override
    329     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    330         if (mCallback != null) {
    331             mCallback.userActivity();
    332         }
    333     }
    334 
    335     @Override
    336     public void onTextChanged(CharSequence s, int start, int before, int count) {
    337     }
    338 
    339     @Override
    340     public void afterTextChanged(Editable s) {
    341         // Poor man's user edit detection, assuming empty text is programmatic and everything else
    342         // is from the user.
    343         if (!TextUtils.isEmpty(s)) {
    344             onUserInput();
    345         }
    346     }
    347 
    348     @Override
    349     public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
    350         // Check if this was the result of hitting the enter key
    351         final boolean isSoftImeEvent = event == null
    352                 && (actionId == EditorInfo.IME_NULL
    353                 || actionId == EditorInfo.IME_ACTION_DONE
    354                 || actionId == EditorInfo.IME_ACTION_NEXT);
    355         final boolean isKeyboardEnterKey = event != null
    356                 && KeyEvent.isConfirmKey(event.getKeyCode())
    357                 && event.getAction() == KeyEvent.ACTION_DOWN;
    358         if (isSoftImeEvent || isKeyboardEnterKey) {
    359             verifyPasswordAndUnlock();
    360             return true;
    361         }
    362         return false;
    363     }
    364 }
    365