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(R.string.kg_password_instructions, false); 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 = (TextView) 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