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 KeyguardPasswordView(Context context)65 public KeyguardPasswordView(Context context) { 66 this(context, null); 67 } 68 KeyguardPasswordView(Context context, AttributeSet attrs)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 resetState()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 getPasswordTextViewId()101 protected int getPasswordTextViewId() { 102 return R.id.passwordEntry; 103 } 104 105 @Override needsInput()106 public boolean needsInput() { 107 return true; 108 } 109 110 @Override onResume(final int reason)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 getPromptReasonStringRes(int reason)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 onPause()147 public void onPause() { 148 super.onPause(); 149 mImm.hideSoftInputFromWindow(getWindowToken(), 0); 150 } 151 updateSwitchImeButton()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 onFinishInflate()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 onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)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 resetPasswordText(boolean animate, boolean announce)241 protected void resetPasswordText(boolean animate, boolean announce) { 242 mPasswordEntry.setText(""); 243 } 244 245 @Override getPasswordText()246 protected byte[] getPasswordText() { 247 return charSequenceToByteArray(mPasswordEntry.getText()); 248 } 249 250 @Override setPasswordEntryEnabled(boolean enabled)251 protected void setPasswordEntryEnabled(boolean enabled) { 252 mPasswordEntry.setEnabled(enabled); 253 } 254 255 @Override setPasswordEntryInputEnabled(boolean enabled)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 */ hasMultipleEnabledIMEsOrSubtypes(InputMethodManager imm, final boolean shouldIncludeAuxiliarySubtypes)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 showUsabilityHint()310 public void showUsabilityHint() { 311 } 312 313 @Override getWrongPasswordStringId()314 public int getWrongPasswordStringId() { 315 return R.string.kg_wrong_password; 316 } 317 318 @Override startAppearAnimation()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 startDisappearAnimation(Runnable finishRunnable)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 beforeTextChanged(CharSequence s, int start, int count, int after)341 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 342 if (mCallback != null) { 343 mCallback.userActivity(); 344 } 345 } 346 347 @Override onTextChanged(CharSequence s, int start, int before, int count)348 public void onTextChanged(CharSequence s, int start, int before, int count) { 349 } 350 351 @Override afterTextChanged(Editable s)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 onEditorAction(TextView v, int actionId, KeyEvent event)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 getTitle()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 */ charSequenceToByteArray(CharSequence chars)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