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