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