1 /* 2 * Copyright (C) 2021 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.server.inputmethod; 18 19 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.app.PendingIntent; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.ServiceConnection; 28 import android.content.pm.PackageManagerInternal; 29 import android.inputmethodservice.InputMethodService; 30 import android.os.Binder; 31 import android.os.IBinder; 32 import android.os.Process; 33 import android.os.SystemClock; 34 import android.os.Trace; 35 import android.os.UserHandle; 36 import android.provider.Settings; 37 import android.util.ArrayMap; 38 import android.util.EventLog; 39 import android.util.Slog; 40 import android.view.WindowManager; 41 import android.view.inputmethod.InputMethod; 42 import android.view.inputmethod.InputMethodInfo; 43 import android.view.inputmethod.InputMethodManager; 44 45 import com.android.internal.annotations.GuardedBy; 46 import com.android.internal.annotations.VisibleForTesting; 47 import com.android.internal.inputmethod.IInputMethod; 48 import com.android.internal.inputmethod.InputBindResult; 49 import com.android.internal.inputmethod.UnbindReason; 50 import com.android.server.EventLogTags; 51 import com.android.server.wm.WindowManagerInternal; 52 53 import java.util.concurrent.CountDownLatch; 54 55 /** 56 * A controller managing the state of the input method binding. 57 */ 58 final class InputMethodBindingController { 59 static final boolean DEBUG = false; 60 private static final String TAG = InputMethodBindingController.class.getSimpleName(); 61 62 /** Time in milliseconds that the IME service has to bind before it is reconnected. */ 63 static final long TIME_TO_RECONNECT = 3 * 1000; 64 65 @NonNull private final InputMethodManagerService mService; 66 @NonNull private final Context mContext; 67 @NonNull private final ArrayMap<String, InputMethodInfo> mMethodMap; 68 @NonNull private final InputMethodUtils.InputMethodSettings mSettings; 69 @NonNull private final PackageManagerInternal mPackageManagerInternal; 70 @NonNull private final WindowManagerInternal mWindowManagerInternal; 71 72 @GuardedBy("ImfLock.class") private long mLastBindTime; 73 @GuardedBy("ImfLock.class") private boolean mHasConnection; 74 @GuardedBy("ImfLock.class") @Nullable private String mCurId; 75 @GuardedBy("ImfLock.class") @Nullable private String mSelectedMethodId; 76 @GuardedBy("ImfLock.class") @Nullable private Intent mCurIntent; 77 @GuardedBy("ImfLock.class") @Nullable private IInputMethodInvoker mCurMethod; 78 @GuardedBy("ImfLock.class") private int mCurMethodUid = Process.INVALID_UID; 79 @GuardedBy("ImfLock.class") @Nullable private IBinder mCurToken; 80 @GuardedBy("ImfLock.class") private int mCurSeq; 81 @GuardedBy("ImfLock.class") private boolean mVisibleBound; 82 @GuardedBy("ImfLock.class") private boolean mSupportsStylusHw; 83 84 @Nullable private CountDownLatch mLatchForTesting; 85 86 /** 87 * Binding flags for establishing connection to the {@link InputMethodService}. 88 */ 89 @VisibleForTesting 90 static final int IME_CONNECTION_BIND_FLAGS = 91 Context.BIND_AUTO_CREATE 92 | Context.BIND_NOT_VISIBLE 93 | Context.BIND_NOT_FOREGROUND 94 | Context.BIND_IMPORTANT_BACKGROUND 95 | Context.BIND_SCHEDULE_LIKE_TOP_APP; 96 97 private final int mImeConnectionBindFlags; 98 99 /** 100 * Binding flags used only while the {@link InputMethodService} is showing window. 101 */ 102 @VisibleForTesting 103 static final int IME_VISIBLE_BIND_FLAGS = 104 Context.BIND_AUTO_CREATE 105 | Context.BIND_TREAT_LIKE_ACTIVITY 106 | Context.BIND_FOREGROUND_SERVICE 107 | Context.BIND_INCLUDE_CAPABILITIES 108 | Context.BIND_SHOWING_UI; 109 InputMethodBindingController(@onNull InputMethodManagerService service)110 InputMethodBindingController(@NonNull InputMethodManagerService service) { 111 this(service, IME_CONNECTION_BIND_FLAGS, null /* latchForTesting */); 112 } 113 InputMethodBindingController(@onNull InputMethodManagerService service, int imeConnectionBindFlags, CountDownLatch latchForTesting)114 InputMethodBindingController(@NonNull InputMethodManagerService service, 115 int imeConnectionBindFlags, CountDownLatch latchForTesting) { 116 mService = service; 117 mContext = mService.mContext; 118 mMethodMap = mService.mMethodMap; 119 mSettings = mService.mSettings; 120 mPackageManagerInternal = mService.mPackageManagerInternal; 121 mWindowManagerInternal = mService.mWindowManagerInternal; 122 mImeConnectionBindFlags = imeConnectionBindFlags; 123 mLatchForTesting = latchForTesting; 124 } 125 126 /** 127 * Time that we last initiated a bind to the input method, to determine 128 * if we should try to disconnect and reconnect to it. 129 */ 130 @GuardedBy("ImfLock.class") getLastBindTime()131 long getLastBindTime() { 132 return mLastBindTime; 133 } 134 135 /** 136 * Set to true if our ServiceConnection is currently actively bound to 137 * a service (whether or not we have gotten its IBinder back yet). 138 */ 139 @GuardedBy("ImfLock.class") hasConnection()140 boolean hasConnection() { 141 return mHasConnection; 142 } 143 144 /** 145 * Id obtained with {@link InputMethodInfo#getId()} for the input method that we are currently 146 * connected to or in the process of connecting to. 147 * 148 * <p>This can be {@code null} when no input method is connected.</p> 149 * 150 * @see #getSelectedMethodId() 151 */ 152 @GuardedBy("ImfLock.class") 153 @Nullable getCurId()154 String getCurId() { 155 return mCurId; 156 } 157 158 /** 159 * Id obtained with {@link InputMethodInfo#getId()} for the currently selected input method. 160 * This is to be synchronized with the secure settings keyed with 161 * {@link android.provider.Settings.Secure#DEFAULT_INPUT_METHOD}. 162 * 163 * <p>This can be transiently {@code null} when the system is re-initializing input method 164 * settings, e.g., the system locale is just changed.</p> 165 * 166 * <p>Note that {@link #getCurId()} is used to track which IME is being connected to 167 * {@link com.android.server.inputmethod.InputMethodManagerService}.</p> 168 * 169 * @see #getCurId() 170 */ 171 @GuardedBy("ImfLock.class") 172 @Nullable getSelectedMethodId()173 String getSelectedMethodId() { 174 return mSelectedMethodId; 175 } 176 177 @GuardedBy("ImfLock.class") setSelectedMethodId(@ullable String selectedMethodId)178 void setSelectedMethodId(@Nullable String selectedMethodId) { 179 mSelectedMethodId = selectedMethodId; 180 } 181 182 /** 183 * The token we have made for the currently active input method, to 184 * identify it in the future. 185 */ 186 @GuardedBy("ImfLock.class") 187 @Nullable getCurToken()188 IBinder getCurToken() { 189 return mCurToken; 190 } 191 192 /** 193 * The Intent used to connect to the current input method. 194 */ 195 @GuardedBy("ImfLock.class") 196 @Nullable getCurIntent()197 Intent getCurIntent() { 198 return mCurIntent; 199 } 200 201 /** 202 * The current binding sequence number, incremented every time there is 203 * a new bind performed. 204 */ 205 @GuardedBy("ImfLock.class") getSequenceNumber()206 int getSequenceNumber() { 207 return mCurSeq; 208 } 209 210 /** 211 * Increase the current binding sequence number by one. 212 * Reset to 1 on overflow. 213 */ 214 @GuardedBy("ImfLock.class") advanceSequenceNumber()215 void advanceSequenceNumber() { 216 mCurSeq += 1; 217 if (mCurSeq <= 0) { 218 mCurSeq = 1; 219 } 220 } 221 222 /** 223 * If non-null, this is the input method service we are currently connected 224 * to. 225 */ 226 @GuardedBy("ImfLock.class") 227 @Nullable getCurMethod()228 IInputMethodInvoker getCurMethod() { 229 return mCurMethod; 230 } 231 232 /** 233 * If not {@link Process#INVALID_UID}, then the UID of {@link #getCurIntent()}. 234 */ 235 @GuardedBy("ImfLock.class") getCurMethodUid()236 int getCurMethodUid() { 237 return mCurMethodUid; 238 } 239 240 /** 241 * Indicates whether {@link #mVisibleConnection} is currently in use. 242 */ 243 @GuardedBy("ImfLock.class") isVisibleBound()244 boolean isVisibleBound() { 245 return mVisibleBound; 246 } 247 248 /** 249 * Returns {@code true} if current IME supports Stylus Handwriting. 250 */ supportsStylusHandwriting()251 boolean supportsStylusHandwriting() { 252 return mSupportsStylusHw; 253 } 254 255 /** 256 * Used to bring IME service up to visible adjustment while it is being shown. 257 */ 258 @GuardedBy("ImfLock.class") 259 private final ServiceConnection mVisibleConnection = new ServiceConnection() { 260 @Override public void onBindingDied(ComponentName name) { 261 synchronized (ImfLock.class) { 262 mService.invalidateAutofillSessionLocked(); 263 if (isVisibleBound()) { 264 unbindVisibleConnection(); 265 } 266 } 267 } 268 269 @Override public void onServiceConnected(ComponentName name, IBinder service) { 270 } 271 272 @Override public void onServiceDisconnected(ComponentName name) { 273 synchronized (ImfLock.class) { 274 mService.invalidateAutofillSessionLocked(); 275 } 276 } 277 }; 278 279 /** 280 * Used to bind the IME while it is not currently being shown. 281 */ 282 @GuardedBy("ImfLock.class") 283 private final ServiceConnection mMainConnection = new ServiceConnection() { 284 @Override 285 public void onServiceConnected(ComponentName name, IBinder service) { 286 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.onServiceConnected"); 287 synchronized (ImfLock.class) { 288 if (mCurIntent != null && name.equals(mCurIntent.getComponent())) { 289 mCurMethod = IInputMethodInvoker.create(IInputMethod.Stub.asInterface(service)); 290 updateCurrentMethodUid(); 291 if (mCurToken == null) { 292 Slog.w(TAG, "Service connected without a token!"); 293 unbindCurrentMethod(); 294 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); 295 return; 296 } 297 if (DEBUG) Slog.v(TAG, "Initiating attach with token: " + mCurToken); 298 final InputMethodInfo info = mMethodMap.get(mSelectedMethodId); 299 boolean supportsStylusHwChanged = 300 mSupportsStylusHw != info.supportsStylusHandwriting(); 301 mSupportsStylusHw = info.supportsStylusHandwriting(); 302 if (supportsStylusHwChanged) { 303 InputMethodManager.invalidateLocalStylusHandwritingAvailabilityCaches(); 304 } 305 mService.initializeImeLocked(mCurMethod, mCurToken); 306 mService.scheduleNotifyImeUidToAudioService(mCurMethodUid); 307 mService.reRequestCurrentClientSessionLocked(); 308 mService.performOnCreateInlineSuggestionsRequestLocked(); 309 } 310 311 // reset Handwriting event receiver. 312 // always call this as it handles changes in mSupportsStylusHw. It is a noop 313 // if unchanged. 314 mService.scheduleResetStylusHandwriting(); 315 } 316 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); 317 318 if (mLatchForTesting != null) { 319 mLatchForTesting.countDown(); // Notify the finish to tests 320 } 321 } 322 323 @GuardedBy("ImfLock.class") 324 private void updateCurrentMethodUid() { 325 final String curMethodPackage = mCurIntent.getComponent().getPackageName(); 326 final int curMethodUid = mPackageManagerInternal.getPackageUid( 327 curMethodPackage, 0 /* flags */, mSettings.getCurrentUserId()); 328 if (curMethodUid < 0) { 329 Slog.e(TAG, "Failed to get UID for package=" + curMethodPackage); 330 mCurMethodUid = Process.INVALID_UID; 331 } else { 332 mCurMethodUid = curMethodUid; 333 } 334 } 335 336 @Override 337 public void onServiceDisconnected(@NonNull ComponentName name) { 338 // Note that mContext.unbindService(this) does not trigger this. Hence if we are 339 // here the 340 // disconnection is not intended by IMMS (e.g. triggered because the current IMS 341 // crashed), 342 // which is irregular but can eventually happen for everyone just by continuing 343 // using the 344 // device. Thus it is important to make sure that all the internal states are 345 // properly 346 // refreshed when this method is called back. Running 347 // adb install -r <APK that implements the current IME> 348 // would be a good way to trigger such a situation. 349 synchronized (ImfLock.class) { 350 if (DEBUG) { 351 Slog.v(TAG, "Service disconnected: " + name + " mCurIntent=" + mCurIntent); 352 } 353 if (mCurMethod != null && mCurIntent != null 354 && name.equals(mCurIntent.getComponent())) { 355 // We consider this to be a new bind attempt, since the system 356 // should now try to restart the service for us. 357 mLastBindTime = SystemClock.uptimeMillis(); 358 clearCurMethodAndSessions(); 359 mService.clearInputShownLocked(); 360 mService.unbindCurrentClientLocked(UnbindReason.DISCONNECT_IME); 361 } 362 } 363 } 364 }; 365 366 @GuardedBy("ImfLock.class") unbindCurrentMethod()367 void unbindCurrentMethod() { 368 if (isVisibleBound()) { 369 unbindVisibleConnection(); 370 } 371 372 if (hasConnection()) { 373 unbindMainConnection(); 374 } 375 376 if (getCurToken() != null) { 377 removeCurrentToken(); 378 mService.resetSystemUiLocked(); 379 } 380 381 mCurId = null; 382 clearCurMethodAndSessions(); 383 } 384 385 @GuardedBy("ImfLock.class") clearCurMethodAndSessions()386 private void clearCurMethodAndSessions() { 387 mService.clearClientSessionsLocked(); 388 mCurMethod = null; 389 mCurMethodUid = Process.INVALID_UID; 390 } 391 392 @GuardedBy("ImfLock.class") removeCurrentToken()393 private void removeCurrentToken() { 394 int curTokenDisplayId = mService.getCurTokenDisplayIdLocked(); 395 396 if (DEBUG) { 397 Slog.v(TAG, 398 "Removing window token: " + mCurToken + " for display: " + curTokenDisplayId); 399 } 400 mWindowManagerInternal.removeWindowToken(mCurToken, false /* removeWindows */, 401 false /* animateExit */, curTokenDisplayId); 402 mCurToken = null; 403 } 404 405 @GuardedBy("ImfLock.class") 406 @NonNull bindCurrentMethod()407 InputBindResult bindCurrentMethod() { 408 if (mSelectedMethodId == null) { 409 Slog.e(TAG, "mSelectedMethodId is null!"); 410 return InputBindResult.NO_IME; 411 } 412 413 InputMethodInfo info = mMethodMap.get(mSelectedMethodId); 414 if (info == null) { 415 throw new IllegalArgumentException("Unknown id: " + mSelectedMethodId); 416 } 417 418 mCurIntent = createImeBindingIntent(info.getComponent()); 419 420 if (bindCurrentInputMethodServiceMainConnection()) { 421 mCurId = info.getId(); 422 mLastBindTime = SystemClock.uptimeMillis(); 423 424 addFreshWindowToken(); 425 return new InputBindResult( 426 InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING, 427 null, null, null, mCurId, mCurSeq, null, false); 428 } 429 430 Slog.w(InputMethodManagerService.TAG, 431 "Failure connecting to input method service: " + mCurIntent); 432 mCurIntent = null; 433 return InputBindResult.IME_NOT_CONNECTED; 434 } 435 436 @NonNull createImeBindingIntent(ComponentName component)437 private Intent createImeBindingIntent(ComponentName component) { 438 Intent intent = new Intent(InputMethod.SERVICE_INTERFACE); 439 intent.setComponent(component); 440 intent.putExtra(Intent.EXTRA_CLIENT_LABEL, 441 com.android.internal.R.string.input_method_binding_label); 442 intent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity( 443 mContext, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS), 444 PendingIntent.FLAG_IMMUTABLE)); 445 return intent; 446 } 447 448 @GuardedBy("ImfLock.class") addFreshWindowToken()449 private void addFreshWindowToken() { 450 int displayIdToShowIme = mService.getDisplayIdToShowImeLocked(); 451 mCurToken = new Binder(); 452 453 mService.setCurTokenDisplayIdLocked(displayIdToShowIme); 454 455 if (DEBUG) { 456 Slog.v(TAG, "Adding window token: " + mCurToken + " for display: " 457 + displayIdToShowIme); 458 } 459 mWindowManagerInternal.addWindowToken(mCurToken, 460 WindowManager.LayoutParams.TYPE_INPUT_METHOD, 461 displayIdToShowIme, null /* options */); 462 } 463 464 @GuardedBy("ImfLock.class") unbindMainConnection()465 private void unbindMainConnection() { 466 mContext.unbindService(mMainConnection); 467 mHasConnection = false; 468 } 469 470 @GuardedBy("ImfLock.class") unbindVisibleConnection()471 void unbindVisibleConnection() { 472 mContext.unbindService(mVisibleConnection); 473 mVisibleBound = false; 474 } 475 476 @GuardedBy("ImfLock.class") bindCurrentInputMethodService(ServiceConnection conn, int flags)477 private boolean bindCurrentInputMethodService(ServiceConnection conn, int flags) { 478 if (mCurIntent == null || conn == null) { 479 Slog.e(TAG, "--- bind failed: service = " + mCurIntent + ", conn = " + conn); 480 return false; 481 } 482 return mContext.bindServiceAsUser(mCurIntent, conn, flags, 483 new UserHandle(mSettings.getCurrentUserId())); 484 } 485 486 @GuardedBy("ImfLock.class") bindCurrentInputMethodServiceMainConnection()487 private boolean bindCurrentInputMethodServiceMainConnection() { 488 mHasConnection = bindCurrentInputMethodService(mMainConnection, mImeConnectionBindFlags); 489 return mHasConnection; 490 } 491 492 /** 493 * Bind the IME so that it can be shown. 494 * 495 * <p> 496 * Performs a rebind if no binding is achieved in {@link #TIME_TO_RECONNECT} milliseconds. 497 */ 498 @GuardedBy("ImfLock.class") setCurrentMethodVisible()499 void setCurrentMethodVisible() { 500 if (mCurMethod != null) { 501 if (DEBUG) Slog.d(TAG, "setCurrentMethodVisible: mCurToken=" + mCurToken); 502 if (hasConnection() && !isVisibleBound()) { 503 mVisibleBound = bindCurrentInputMethodService(mVisibleConnection, 504 IME_VISIBLE_BIND_FLAGS); 505 } 506 return; 507 } 508 509 // No IME is currently connected. Reestablish the main connection. 510 if (!hasConnection()) { 511 if (DEBUG) { 512 Slog.d(TAG, "Cannot show input: no IME bound. Rebinding."); 513 } 514 bindCurrentMethod(); 515 return; 516 } 517 518 long bindingDuration = SystemClock.uptimeMillis() - mLastBindTime; 519 if (bindingDuration >= TIME_TO_RECONNECT) { 520 // The client has asked to have the input method shown, but 521 // we have been sitting here too long with a connection to the 522 // service and no interface received, so let's disconnect/connect 523 // to try to prod things along. 524 EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME, getSelectedMethodId(), 525 bindingDuration, 1); 526 Slog.w(TAG, "Force disconnect/connect to the IME in setCurrentMethodVisible()"); 527 unbindMainConnection(); 528 bindCurrentInputMethodServiceMainConnection(); 529 } else { 530 if (DEBUG) { 531 Slog.d(TAG, "Can't show input: connection = " + mHasConnection + ", time = " 532 + (TIME_TO_RECONNECT - bindingDuration)); 533 } 534 } 535 } 536 537 /** 538 * Remove the binding needed for the IME to be shown. 539 */ 540 @GuardedBy("ImfLock.class") setCurrentMethodNotVisible()541 void setCurrentMethodNotVisible() { 542 if (isVisibleBound()) { 543 unbindVisibleConnection(); 544 } 545 } 546 } 547