1 /* 2 * Copyright (C) 2020 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.res.ColorStateList; 20 import android.content.res.Resources; 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.view.KeyEvent; 28 import android.view.View; 29 import android.view.ViewGroup.MarginLayoutParams; 30 import android.view.WindowInsets; 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.EditText; 36 import android.widget.ImageView; 37 import android.widget.TextView.OnEditorActionListener; 38 39 import com.android.internal.util.LatencyTracker; 40 import com.android.internal.widget.LockPatternUtils; 41 import com.android.keyguard.KeyguardSecurityModel.SecurityMode; 42 import com.android.settingslib.Utils; 43 import com.android.systemui.R; 44 import com.android.systemui.classifier.FalsingCollector; 45 import com.android.systemui.dagger.qualifiers.Main; 46 import com.android.systemui.util.concurrency.DelayableExecutor; 47 48 import java.util.List; 49 50 public class KeyguardPasswordViewController 51 extends KeyguardAbsKeyInputViewController<KeyguardPasswordView> { 52 53 private static final int DELAY_MILLIS_TO_REEVALUATE_IME_SWITCH_ICON = 500; // 500ms 54 55 private final KeyguardSecurityCallback mKeyguardSecurityCallback; 56 private final InputMethodManager mInputMethodManager; 57 private final DelayableExecutor mMainExecutor; 58 private final boolean mShowImeAtScreenOn; 59 private EditText mPasswordEntry; 60 private ImageView mSwitchImeButton; 61 62 private final OnEditorActionListener mOnEditorActionListener = (v, actionId, event) -> { 63 // Check if this was the result of hitting the enter key 64 final boolean isSoftImeEvent = event == null 65 && (actionId == EditorInfo.IME_NULL 66 || actionId == EditorInfo.IME_ACTION_DONE 67 || actionId == EditorInfo.IME_ACTION_NEXT); 68 final boolean isKeyboardEnterKey = event != null 69 && KeyEvent.isConfirmKey(event.getKeyCode()) 70 && event.getAction() == KeyEvent.ACTION_DOWN; 71 if (isSoftImeEvent || isKeyboardEnterKey) { 72 verifyPasswordAndUnlock(); 73 return true; 74 } 75 return false; 76 }; 77 78 private final TextWatcher mTextWatcher = new TextWatcher() { 79 @Override 80 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 81 mKeyguardSecurityCallback.userActivity(); 82 } 83 84 @Override 85 public void onTextChanged(CharSequence s, int start, int before, int count) { 86 } 87 88 @Override 89 public void afterTextChanged(Editable s) { 90 if (!TextUtils.isEmpty(s)) { 91 onUserInput(); 92 } 93 } 94 }; 95 96 @Override reloadColors()97 public void reloadColors() { 98 super.reloadColors(); 99 int textColor = Utils.getColorAttr(mView.getContext(), 100 android.R.attr.textColorPrimary).getDefaultColor(); 101 mPasswordEntry.setTextColor(textColor); 102 mPasswordEntry.setHighlightColor(textColor); 103 mPasswordEntry.setBackgroundTintList(ColorStateList.valueOf(textColor)); 104 mPasswordEntry.setForegroundTintList(ColorStateList.valueOf(textColor)); 105 mSwitchImeButton.setImageTintList(ColorStateList.valueOf(textColor)); 106 } 107 KeyguardPasswordViewController(KeyguardPasswordView view, KeyguardUpdateMonitor keyguardUpdateMonitor, SecurityMode securityMode, LockPatternUtils lockPatternUtils, KeyguardSecurityCallback keyguardSecurityCallback, KeyguardMessageAreaController.Factory messageAreaControllerFactory, LatencyTracker latencyTracker, InputMethodManager inputMethodManager, EmergencyButtonController emergencyButtonController, @Main DelayableExecutor mainExecutor, @Main Resources resources, FalsingCollector falsingCollector)108 protected KeyguardPasswordViewController(KeyguardPasswordView view, 109 KeyguardUpdateMonitor keyguardUpdateMonitor, 110 SecurityMode securityMode, 111 LockPatternUtils lockPatternUtils, 112 KeyguardSecurityCallback keyguardSecurityCallback, 113 KeyguardMessageAreaController.Factory messageAreaControllerFactory, 114 LatencyTracker latencyTracker, 115 InputMethodManager inputMethodManager, 116 EmergencyButtonController emergencyButtonController, 117 @Main DelayableExecutor mainExecutor, 118 @Main Resources resources, 119 FalsingCollector falsingCollector) { 120 super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, 121 messageAreaControllerFactory, latencyTracker, falsingCollector, 122 emergencyButtonController); 123 mKeyguardSecurityCallback = keyguardSecurityCallback; 124 mInputMethodManager = inputMethodManager; 125 mMainExecutor = mainExecutor; 126 mShowImeAtScreenOn = resources.getBoolean(R.bool.kg_show_ime_at_screen_on); 127 mPasswordEntry = mView.findViewById(mView.getPasswordTextViewId()); 128 mSwitchImeButton = mView.findViewById(R.id.switch_ime_button); 129 } 130 131 @Override onViewAttached()132 protected void onViewAttached() { 133 super.onViewAttached(); 134 mPasswordEntry.setTextOperationUser(UserHandle.of(KeyguardUpdateMonitor.getCurrentUser())); 135 mPasswordEntry.setKeyListener(TextKeyListener.getInstance()); 136 mPasswordEntry.setInputType(InputType.TYPE_CLASS_TEXT 137 | InputType.TYPE_TEXT_VARIATION_PASSWORD); 138 139 // Set selected property on so the view can send accessibility events. 140 mPasswordEntry.setSelected(true); 141 mPasswordEntry.setOnEditorActionListener(mOnEditorActionListener); 142 mPasswordEntry.addTextChangedListener(mTextWatcher); 143 // Poke the wakelock any time the text is selected or modified 144 mPasswordEntry.setOnClickListener(v -> mKeyguardSecurityCallback.userActivity()); 145 146 mSwitchImeButton.setOnClickListener(v -> { 147 mKeyguardSecurityCallback.userActivity(); // Leave the screen on a bit longer 148 // Do not show auxiliary subtypes in password lock screen. 149 mInputMethodManager.showInputMethodPickerFromSystem(false, 150 mView.getContext().getDisplayId()); 151 }); 152 153 View cancelBtn = mView.findViewById(R.id.cancel_button); 154 if (cancelBtn != null) { 155 cancelBtn.setOnClickListener(view -> { 156 mKeyguardSecurityCallback.reset(); 157 mKeyguardSecurityCallback.onCancelClicked(); 158 }); 159 } 160 161 // If there's more than one IME, enable the IME switcher button 162 updateSwitchImeButton(); 163 164 // When we the current user is switching, InputMethodManagerService sometimes has not 165 // switched internal state yet here. As a quick workaround, we check the keyboard state 166 // again. 167 // TODO: Remove this workaround by ensuring such a race condition never happens. 168 mMainExecutor.executeDelayed( 169 this::updateSwitchImeButton, DELAY_MILLIS_TO_REEVALUATE_IME_SWITCH_ICON); 170 } 171 172 @Override onViewDetached()173 protected void onViewDetached() { 174 super.onViewDetached(); 175 mPasswordEntry.setOnEditorActionListener(null); 176 } 177 178 @Override needsInput()179 public boolean needsInput() { 180 return true; 181 } 182 183 @Override resetState()184 void resetState() { 185 mPasswordEntry.setTextOperationUser(UserHandle.of(KeyguardUpdateMonitor.getCurrentUser())); 186 mMessageAreaController.setMessage(""); 187 final boolean wasDisabled = mPasswordEntry.isEnabled(); 188 mView.setPasswordEntryEnabled(true); 189 mView.setPasswordEntryInputEnabled(true); 190 // Don't call showSoftInput when PasswordEntry is invisible or in pausing stage. 191 if (!mResumed || !mPasswordEntry.isVisibleToUser()) { 192 return; 193 } 194 if (wasDisabled) { 195 showInput(); 196 } 197 } 198 199 @Override onResume(int reason)200 public void onResume(int reason) { 201 super.onResume(reason); 202 if (reason != KeyguardSecurityView.SCREEN_ON || mShowImeAtScreenOn) { 203 showInput(); 204 } 205 } 206 showInput()207 private void showInput() { 208 mView.post(() -> { 209 if (mView.isShown()) { 210 mPasswordEntry.requestFocus(); 211 mPasswordEntry.getWindowInsetsController().show(WindowInsets.Type.ime()); 212 } 213 }); 214 } 215 216 @Override onPause()217 public void onPause() { 218 if (!mPasswordEntry.isVisibleToUser()) { 219 // Reset all states directly and then hide IME when the screen turned off. 220 super.onPause(); 221 } else { 222 // In order not to break the IME hide animation by resetting states too early after 223 // the password checked, make sure resetting states after the IME hiding animation 224 // finished. 225 mView.setOnFinishImeAnimationRunnable(() -> { 226 mPasswordEntry.clearFocus(); 227 super.onPause(); 228 }); 229 } 230 if (mPasswordEntry.isAttachedToWindow()) { 231 mPasswordEntry.getWindowInsetsController().hide(WindowInsets.Type.ime()); 232 } 233 } 234 235 @Override onStartingToHide()236 public void onStartingToHide() { 237 if (mPasswordEntry.isAttachedToWindow()) { 238 mPasswordEntry.getWindowInsetsController().hide(WindowInsets.Type.ime()); 239 } 240 } 241 updateSwitchImeButton()242 private void updateSwitchImeButton() { 243 // If there's more than one IME, enable the IME switcher button 244 final boolean wasVisible = mSwitchImeButton.getVisibility() == View.VISIBLE; 245 final boolean shouldBeVisible = hasMultipleEnabledIMEsOrSubtypes( 246 mInputMethodManager, false); 247 if (wasVisible != shouldBeVisible) { 248 mSwitchImeButton.setVisibility(shouldBeVisible ? View.VISIBLE : View.GONE); 249 } 250 251 // TODO: Check if we still need this hack. 252 // If no icon is visible, reset the start margin on the password field so the text is 253 // still centered. 254 if (mSwitchImeButton.getVisibility() != View.VISIBLE) { 255 android.view.ViewGroup.LayoutParams params = mPasswordEntry.getLayoutParams(); 256 if (params instanceof MarginLayoutParams) { 257 final MarginLayoutParams mlp = (MarginLayoutParams) params; 258 mlp.setMarginStart(0); 259 mPasswordEntry.setLayoutParams(params); 260 } 261 } 262 } 263 264 /** 265 * Method adapted from com.android.inputmethod.latin.Utils 266 * 267 * @param imm The input method manager 268 * @param shouldIncludeAuxiliarySubtypes 269 * @return true if we have multiple IMEs to choose from 270 */ hasMultipleEnabledIMEsOrSubtypes(InputMethodManager imm, final boolean shouldIncludeAuxiliarySubtypes)271 private boolean hasMultipleEnabledIMEsOrSubtypes(InputMethodManager imm, 272 final boolean shouldIncludeAuxiliarySubtypes) { 273 final List<InputMethodInfo> enabledImis = 274 imm.getEnabledInputMethodListAsUser(KeyguardUpdateMonitor.getCurrentUser()); 275 276 // Number of the filtered IMEs 277 int filteredImisCount = 0; 278 279 for (InputMethodInfo imi : enabledImis) { 280 // We can return true immediately after we find two or more filtered IMEs. 281 if (filteredImisCount > 1) return true; 282 final List<InputMethodSubtype> subtypes = 283 imm.getEnabledInputMethodSubtypeList(imi, true); 284 // IMEs that have no subtypes should be counted. 285 if (subtypes.isEmpty()) { 286 ++filteredImisCount; 287 continue; 288 } 289 290 int auxCount = 0; 291 for (InputMethodSubtype subtype : subtypes) { 292 if (subtype.isAuxiliary()) { 293 ++auxCount; 294 } 295 } 296 final int nonAuxCount = subtypes.size() - auxCount; 297 298 // IMEs that have one or more non-auxiliary subtypes should be counted. 299 // If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary 300 // subtypes should be counted as well. 301 if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) { 302 ++filteredImisCount; 303 continue; 304 } 305 } 306 307 return filteredImisCount > 1 308 // imm.getEnabledInputMethodSubtypeList(null, false) will return the current IME's 309 //enabled input method subtype (The current IME should be LatinIME.) 310 || imm.getEnabledInputMethodSubtypeList(null, false).size() > 1; 311 } 312 } 313