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