1 /* 2 * Copyright (C) 2017 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.cts.mockime; 18 19 import static android.server.wm.jetpack.utils.ExtensionUtil.getWindowExtensions; 20 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 21 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 22 import static android.view.WindowInsets.Type.captionBar; 23 import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; 24 25 import android.content.BroadcastReceiver; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.IntentFilter; 30 import android.content.pm.PackageManager; 31 import android.content.res.Configuration; 32 import android.graphics.Bitmap; 33 import android.graphics.RectF; 34 import android.inputmethodservice.InputMethodService; 35 import android.os.Build; 36 import android.os.Bundle; 37 import android.os.CancellationSignal; 38 import android.os.Handler; 39 import android.os.HandlerThread; 40 import android.os.IBinder; 41 import android.os.Looper; 42 import android.os.Process; 43 import android.os.ResultReceiver; 44 import android.os.StrictMode; 45 import android.os.SystemClock; 46 import android.text.TextUtils; 47 import android.util.Log; 48 import android.util.Size; 49 import android.util.TypedValue; 50 import android.view.Display; 51 import android.view.GestureDetector; 52 import android.view.Gravity; 53 import android.view.KeyEvent; 54 import android.view.MotionEvent; 55 import android.view.View; 56 import android.view.ViewConfiguration; 57 import android.view.Window; 58 import android.view.WindowInsets; 59 import android.view.WindowManager; 60 import android.view.inputmethod.CancellableHandwritingGesture; 61 import android.view.inputmethod.CompletionInfo; 62 import android.view.inputmethod.CorrectionInfo; 63 import android.view.inputmethod.CursorAnchorInfo; 64 import android.view.inputmethod.EditorInfo; 65 import android.view.inputmethod.ExtractedTextRequest; 66 import android.view.inputmethod.HandwritingGesture; 67 import android.view.inputmethod.InlineSuggestion; 68 import android.view.inputmethod.InlineSuggestionsRequest; 69 import android.view.inputmethod.InlineSuggestionsResponse; 70 import android.view.inputmethod.InputBinding; 71 import android.view.inputmethod.InputConnection; 72 import android.view.inputmethod.InputContentInfo; 73 import android.view.inputmethod.InputMethod; 74 import android.view.inputmethod.InputMethodManager; 75 import android.view.inputmethod.InputMethodSubtype; 76 import android.view.inputmethod.PreviewableHandwritingGesture; 77 import android.view.inputmethod.TextAttribute; 78 import android.view.inputmethod.TextBoundsInfoResult; 79 import android.widget.FrameLayout; 80 import android.widget.HorizontalScrollView; 81 import android.widget.ImageView; 82 import android.widget.LinearLayout; 83 import android.widget.TextView; 84 import android.widget.inline.InlinePresentationSpec; 85 86 import androidx.annotation.AnyThread; 87 import androidx.annotation.CallSuper; 88 import androidx.annotation.MainThread; 89 import androidx.annotation.NonNull; 90 import androidx.annotation.Nullable; 91 import androidx.annotation.WorkerThread; 92 import androidx.autofill.inline.UiVersions; 93 import androidx.autofill.inline.UiVersions.StylesBuilder; 94 import androidx.autofill.inline.v1.InlineSuggestionUi; 95 import androidx.window.extensions.WindowExtensions; 96 import androidx.window.extensions.layout.WindowLayoutComponent; 97 import androidx.window.extensions.layout.WindowLayoutInfo; 98 99 import java.time.Duration; 100 import java.util.ArrayList; 101 import java.util.concurrent.ExecutorService; 102 import java.util.concurrent.Executors; 103 import java.util.concurrent.atomic.AtomicBoolean; 104 import java.util.concurrent.atomic.AtomicInteger; 105 import java.util.concurrent.atomic.AtomicReference; 106 import java.util.function.BooleanSupplier; 107 import java.util.function.Consumer; 108 import java.util.function.IntConsumer; 109 import java.util.function.Supplier; 110 111 /** 112 * Mock IME for end-to-end tests. 113 */ 114 public final class MockIme extends InputMethodService { 115 116 private static final String TAG = "MockIme"; 117 118 private static final String PACKAGE_NAME = "com.android.cts.mockime"; 119 private static final long DELAY_CANCELLATION_SIGNAL_MILLIS = 500; 120 private ArrayList<MotionEvent> mEvents; 121 122 private View mExtractView; 123 getComponentName()124 static ComponentName getComponentName() { 125 return new ComponentName(PACKAGE_NAME, MockIme.class.getName()); 126 } 127 getImeId()128 static String getImeId() { 129 return getComponentName().flattenToShortString(); 130 } 131 getCommandActionName(@onNull String eventActionName)132 static String getCommandActionName(@NonNull String eventActionName) { 133 return eventActionName + ".command"; 134 } 135 136 @Nullable 137 private final WindowExtensions mWindowExtensions = getWindowExtensions(); 138 @Nullable 139 private final WindowLayoutComponent mWindowLayoutComponent = 140 mWindowExtensions != null ? mWindowExtensions.getWindowLayoutComponent() : null; 141 142 /** The extensions core Consumer to receive {@link WindowLayoutInfo} updates. */ 143 private androidx.window.extensions.core.util.function.Consumer<WindowLayoutInfo> 144 mWindowLayoutInfoConsumer; 145 146 private final HandlerThread mHandlerThread = new HandlerThread("CommandReceiver"); 147 148 private final Handler mMainHandler = new Handler(); 149 150 private final Configuration mLastDispatchedConfiguration = new Configuration(); 151 152 private static final class CommandReceiver extends BroadcastReceiver { 153 @NonNull 154 private final String mActionName; 155 @NonNull 156 private final Consumer<ImeCommand> mOnReceiveCommand; 157 CommandReceiver(@onNull String actionName, @NonNull Consumer<ImeCommand> onReceiveCommand)158 CommandReceiver(@NonNull String actionName, 159 @NonNull Consumer<ImeCommand> onReceiveCommand) { 160 mActionName = actionName; 161 mOnReceiveCommand = onReceiveCommand; 162 } 163 164 @Override onReceive(Context context, Intent intent)165 public void onReceive(Context context, Intent intent) { 166 if (TextUtils.equals(mActionName, intent.getAction())) { 167 mOnReceiveCommand.accept(ImeCommand.fromBundle(intent.getExtras())); 168 } 169 } 170 } 171 172 @Nullable 173 private InputConnection mMemorizedInputConnection = null; 174 175 @Nullable 176 @MainThread getMemorizedOrCurrentInputConnection()177 private InputConnection getMemorizedOrCurrentInputConnection() { 178 return mMemorizedInputConnection != null 179 ? mMemorizedInputConnection : getCurrentInputConnection(); 180 } 181 182 @WorkerThread onReceiveCommand(@onNull ImeCommand command)183 private void onReceiveCommand(@NonNull ImeCommand command) { 184 getTracer().onReceiveCommand(command, () -> { 185 if (command.shouldDispatchToMainThread()) { 186 mMainHandler.post(() -> onHandleCommand(command)); 187 } else { 188 onHandleCommand(command); 189 } 190 }); 191 } 192 193 @AnyThread onHandleCommand(@onNull ImeCommand command)194 private void onHandleCommand(@NonNull ImeCommand command) { 195 getTracer().onHandleCommand(command, () -> { 196 if (command.shouldDispatchToMainThread()) { 197 if (Looper.myLooper() != Looper.getMainLooper()) { 198 throw new IllegalStateException("command " + command 199 + " should be handled on the main thread"); 200 } 201 // The context which created from InputMethodService#createXXXContext must behave 202 // like an UI context, which can obtain a display, a window manager, 203 // a view configuration and a gesture detector instance without strict mode 204 // violation. 205 final Configuration testConfig = new Configuration(); 206 testConfig.setToDefaults(); 207 final Context configContext = createConfigurationContext(testConfig); 208 final Context attrContext = createAttributionContext(null /* attributionTag */); 209 // UI component accesses on a display context must throw strict mode violations. 210 final Context displayContext = createDisplayContext(getDisplay()); 211 switch (command.getName()) { 212 case "suspendCreateSession": { 213 if (!Looper.getMainLooper().isCurrentThread()) { 214 return new UnsupportedOperationException("suspendCreateSession can be" 215 + " requested only for the main thread."); 216 } 217 mSuspendCreateSession = true; 218 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 219 } 220 case "resumeCreateSession": { 221 if (!Looper.getMainLooper().isCurrentThread()) { 222 return new UnsupportedOperationException("resumeCreateSession can be" 223 + " requested only for the main thread."); 224 } 225 if (mSuspendedCreateSession != null) { 226 mSuspendedCreateSession.run(); 227 mSuspendedCreateSession = null; 228 } 229 mSuspendCreateSession = false; 230 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 231 } 232 case "memorizeCurrentInputConnection": { 233 if (!Looper.getMainLooper().isCurrentThread()) { 234 return new UnsupportedOperationException( 235 "memorizeCurrentInputConnection can be requested only for the" 236 + " main thread."); 237 } 238 mMemorizedInputConnection = getCurrentInputConnection(); 239 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 240 } 241 case "unmemorizeCurrentInputConnection": { 242 if (!Looper.getMainLooper().isCurrentThread()) { 243 return new UnsupportedOperationException( 244 "unmemorizeCurrentInputConnection can be requested only for the" 245 + " main thread."); 246 } 247 mMemorizedInputConnection = null; 248 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 249 } 250 case "getTextBeforeCursor": { 251 final int n = command.getExtras().getInt("n"); 252 final int flag = command.getExtras().getInt("flag"); 253 return getMemorizedOrCurrentInputConnection().getTextBeforeCursor(n, flag); 254 } 255 case "getTextAfterCursor": { 256 final int n = command.getExtras().getInt("n"); 257 final int flag = command.getExtras().getInt("flag"); 258 return getMemorizedOrCurrentInputConnection().getTextAfterCursor(n, flag); 259 } 260 case "getSelectedText": { 261 final int flag = command.getExtras().getInt("flag"); 262 return getMemorizedOrCurrentInputConnection().getSelectedText(flag); 263 } 264 case "getSurroundingText": { 265 final int beforeLength = command.getExtras().getInt("beforeLength"); 266 final int afterLength = command.getExtras().getInt("afterLength"); 267 final int flags = command.getExtras().getInt("flags"); 268 return getMemorizedOrCurrentInputConnection().getSurroundingText( 269 beforeLength, afterLength, flags); 270 } 271 case "getCursorCapsMode": { 272 final int reqModes = command.getExtras().getInt("reqModes"); 273 return getMemorizedOrCurrentInputConnection().getCursorCapsMode(reqModes); 274 } 275 case "getExtractedText": { 276 final ExtractedTextRequest request = 277 command.getExtras().getParcelable("request"); 278 final int flags = command.getExtras().getInt("flags"); 279 return getMemorizedOrCurrentInputConnection().getExtractedText(request, 280 flags); 281 } 282 case "deleteSurroundingText": { 283 final int beforeLength = command.getExtras().getInt("beforeLength"); 284 final int afterLength = command.getExtras().getInt("afterLength"); 285 return getMemorizedOrCurrentInputConnection().deleteSurroundingText( 286 beforeLength, afterLength); 287 } 288 case "deleteSurroundingTextInCodePoints": { 289 final int beforeLength = command.getExtras().getInt("beforeLength"); 290 final int afterLength = command.getExtras().getInt("afterLength"); 291 return getMemorizedOrCurrentInputConnection() 292 .deleteSurroundingTextInCodePoints(beforeLength, afterLength); 293 } 294 case "setComposingText(CharSequence,int)": { 295 final CharSequence text = command.getExtras().getCharSequence("text"); 296 final int newCursorPosition = 297 command.getExtras().getInt("newCursorPosition"); 298 return getMemorizedOrCurrentInputConnection().setComposingText( 299 text, newCursorPosition); 300 } 301 case "setComposingText(CharSequence,int,TextAttribute)": { 302 final CharSequence text = command.getExtras().getCharSequence("text"); 303 final int newCursorPosition = 304 command.getExtras().getInt("newCursorPosition"); 305 final TextAttribute textAttribute = 306 command.getExtras().getParcelable("textAttribute"); 307 return getMemorizedOrCurrentInputConnection() 308 .setComposingText(text, newCursorPosition, textAttribute); 309 } 310 case "setComposingRegion(int,int)": { 311 final int start = command.getExtras().getInt("start"); 312 final int end = command.getExtras().getInt("end"); 313 return getMemorizedOrCurrentInputConnection().setComposingRegion(start, 314 end); 315 } 316 case "setComposingRegion(int,int,TextAttribute)": { 317 final int start = command.getExtras().getInt("start"); 318 final int end = command.getExtras().getInt("end"); 319 final TextAttribute textAttribute = 320 command.getExtras().getParcelable("textAttribute"); 321 return getMemorizedOrCurrentInputConnection() 322 .setComposingRegion(start, end, textAttribute); 323 } 324 case "finishComposingText": 325 return getMemorizedOrCurrentInputConnection().finishComposingText(); 326 case "commitText(CharSequence,int)": { 327 final CharSequence text = command.getExtras().getCharSequence("text"); 328 final int newCursorPosition = 329 command.getExtras().getInt("newCursorPosition"); 330 return getMemorizedOrCurrentInputConnection().commitText(text, 331 newCursorPosition); 332 } 333 case "commitText(CharSequence,int,TextAttribute)": { 334 final CharSequence text = command.getExtras().getCharSequence("text"); 335 final int newCursorPosition = 336 command.getExtras().getInt("newCursorPosition"); 337 final TextAttribute textAttribute = 338 command.getExtras().getParcelable("textAttribute"); 339 return getMemorizedOrCurrentInputConnection() 340 .commitText(text, newCursorPosition, textAttribute); 341 } 342 case "commitCompletion": { 343 final CompletionInfo text = command.getExtras().getParcelable("text"); 344 return getMemorizedOrCurrentInputConnection().commitCompletion(text); 345 } 346 case "commitCorrection": { 347 final CorrectionInfo correctionInfo = 348 command.getExtras().getParcelable("correctionInfo"); 349 return getMemorizedOrCurrentInputConnection().commitCorrection( 350 correctionInfo); 351 } 352 case "setSelection": { 353 final int start = command.getExtras().getInt("start"); 354 final int end = command.getExtras().getInt("end"); 355 return getMemorizedOrCurrentInputConnection().setSelection(start, end); 356 } 357 case "performEditorAction": { 358 final int editorAction = command.getExtras().getInt("editorAction"); 359 return getMemorizedOrCurrentInputConnection().performEditorAction( 360 editorAction); 361 } 362 case "performContextMenuAction": { 363 final int id = command.getExtras().getInt("id"); 364 return getMemorizedOrCurrentInputConnection().performContextMenuAction(id); 365 } 366 case "beginBatchEdit": 367 return getMemorizedOrCurrentInputConnection().beginBatchEdit(); 368 case "endBatchEdit": 369 return getMemorizedOrCurrentInputConnection().endBatchEdit(); 370 case "sendKeyEvent": { 371 final KeyEvent event = command.getExtras().getParcelable("event"); 372 return getMemorizedOrCurrentInputConnection().sendKeyEvent(event); 373 } 374 case "clearMetaKeyStates": { 375 final int states = command.getExtras().getInt("states"); 376 return getMemorizedOrCurrentInputConnection().clearMetaKeyStates(states); 377 } 378 case "reportFullscreenMode": { 379 final boolean enabled = command.getExtras().getBoolean("enabled"); 380 return getMemorizedOrCurrentInputConnection().reportFullscreenMode(enabled); 381 } 382 case "performSpellCheck": { 383 return getMemorizedOrCurrentInputConnection().performSpellCheck(); 384 } 385 case "takeSnapshot": { 386 return getMemorizedOrCurrentInputConnection().takeSnapshot(); 387 } 388 case "performPrivateCommand": { 389 final String action = command.getExtras().getString("action"); 390 final Bundle data = command.getExtras().getBundle("data"); 391 return getMemorizedOrCurrentInputConnection().performPrivateCommand(action, 392 data); 393 } 394 case "previewHandwritingGesture": { 395 final PreviewableHandwritingGesture gesture = 396 (PreviewableHandwritingGesture) HandwritingGesture.fromByteArray( 397 command.getExtras().getByteArray("gesture")); 398 399 boolean useDelayedCancellation = 400 command.getExtras().getBoolean("useDelayedCancellation"); 401 final CancellationSignal cs = 402 useDelayedCancellation ? new CancellationSignal() : null; 403 if (useDelayedCancellation) { 404 new Handler().postDelayed(() -> cs.cancel(), 405 DELAY_CANCELLATION_SIGNAL_MILLIS); 406 } 407 408 getMemorizedOrCurrentInputConnection().previewHandwritingGesture( 409 gesture, cs); 410 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 411 } 412 case "performHandwritingGesture": { 413 final HandwritingGesture gesture = 414 HandwritingGesture.fromByteArray( 415 command.getExtras().getByteArray("gesture")); 416 417 boolean useDelayedCancellation = 418 command.getExtras().getBoolean("useDelayedCancellation"); 419 if (useDelayedCancellation 420 && gesture instanceof CancellableHandwritingGesture) { 421 final CancellationSignal cs = new CancellationSignal(); 422 ((CancellableHandwritingGesture) gesture).setCancellationSignal(cs); 423 new Handler().postDelayed(() -> cs.cancel(), 424 DELAY_CANCELLATION_SIGNAL_MILLIS); 425 } 426 427 IntConsumer consumer = value -> 428 getTracer().onPerformHandwritingGestureResult( 429 value, command.getId(), () -> {}); 430 getMemorizedOrCurrentInputConnection() 431 .performHandwritingGesture(gesture, mMainHandler::post, consumer); 432 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 433 } 434 case "requestTextBoundsInfo": { 435 var rectF = command.getExtras().getParcelable("rectF", RectF.class); 436 Consumer<TextBoundsInfoResult> consumer = value -> 437 getTracer().onRequestTextBoundsInfoResult( 438 value, command.getId()); 439 getMemorizedOrCurrentInputConnection().requestTextBoundsInfo( 440 rectF, mMainHandler::post, consumer); 441 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 442 } 443 case "requestCursorUpdates": { 444 final int cursorUpdateMode = command.getExtras().getInt("cursorUpdateMode"); 445 final int cursorUpdateFilter = 446 command.getExtras().getInt("cursorUpdateFilter", 0); 447 return getMemorizedOrCurrentInputConnection().requestCursorUpdates( 448 cursorUpdateMode, cursorUpdateFilter); 449 } 450 case "getHandler": 451 return getMemorizedOrCurrentInputConnection().getHandler(); 452 case "closeConnection": 453 getMemorizedOrCurrentInputConnection().closeConnection(); 454 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 455 case "commitContent": { 456 final InputContentInfo inputContentInfo = 457 command.getExtras().getParcelable("inputContentInfo"); 458 final int flags = command.getExtras().getInt("flags"); 459 final Bundle opts = command.getExtras().getBundle("opts"); 460 return getMemorizedOrCurrentInputConnection().commitContent( 461 inputContentInfo, flags, opts); 462 } 463 case "setImeConsumesInput": { 464 final boolean imeConsumesInput = 465 command.getExtras().getBoolean("imeConsumesInput"); 466 return getMemorizedOrCurrentInputConnection().setImeConsumesInput( 467 imeConsumesInput); 468 } 469 case "replaceText": { 470 final int start = command.getExtras().getInt("start"); 471 final int end = command.getExtras().getInt("end"); 472 final CharSequence text = command.getExtras().getCharSequence("text"); 473 final int newCursorPosition = 474 command.getExtras().getInt("newCursorPosition"); 475 final TextAttribute textAttribute = 476 command.getExtras().getParcelable("textAttribute"); 477 return getMemorizedOrCurrentInputConnection() 478 .replaceText(start, end, text, newCursorPosition, textAttribute); 479 } 480 case "setExplicitlyEnabledInputMethodSubtypes": { 481 final String imeId = command.getExtras().getString("imeId"); 482 final int[] subtypeHashCodes = 483 command.getExtras().getIntArray("subtypeHashCodes"); 484 getSystemService(InputMethodManager.class) 485 .setExplicitlyEnabledInputMethodSubtypes(imeId, subtypeHashCodes); 486 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 487 } 488 case "switchInputMethod": { 489 final String id = command.getExtras().getString("id"); 490 try { 491 switchInputMethod(id); 492 } catch (Exception e) { 493 return e; 494 } 495 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 496 } 497 case "switchInputMethod(String,InputMethodSubtype)": { 498 final String id = command.getExtras().getString("id"); 499 final InputMethodSubtype subtype = command.getExtras().getParcelable( 500 "subtype", InputMethodSubtype.class); 501 try { 502 switchInputMethod(id, subtype); 503 } catch (Exception e) { 504 return e; 505 } 506 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 507 } 508 case "setBackDisposition": { 509 final int backDisposition = 510 command.getExtras().getInt("backDisposition"); 511 setBackDisposition(backDisposition); 512 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 513 } 514 case "requestHideSelf": { 515 final int flags = command.getExtras().getInt("flags"); 516 requestHideSelf(flags); 517 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 518 } 519 case "requestShowSelf": { 520 final int flags = command.getExtras().getInt("flags"); 521 requestShowSelf(flags); 522 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 523 } 524 case "sendDownUpKeyEvents": { 525 final int keyEventCode = command.getExtras().getInt("keyEventCode"); 526 sendDownUpKeyEvents(keyEventCode); 527 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 528 } 529 case "getApplicationInfo": { 530 final String packageName = command.getExtras().getString("packageName"); 531 final int flags = command.getExtras().getInt("flags"); 532 try { 533 return getPackageManager().getApplicationInfo(packageName, flags); 534 } catch (PackageManager.NameNotFoundException e) { 535 return e; 536 } 537 } 538 case "getDisplayId": 539 return getDisplay().getDisplayId(); 540 case "verifyLayoutInflaterContext": 541 return getLayoutInflater().getContext() == this; 542 case "setHeight": 543 final int height = command.getExtras().getInt("height"); 544 mView.setHeight(height); 545 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 546 case "setInlineSuggestionsExtras": 547 mInlineSuggestionsExtras = command.getExtras(); 548 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 549 case "verifyExtractViewNotNull": 550 if (mExtractView == null) { 551 return false; 552 } else { 553 return mExtractView.findViewById(android.R.id.inputExtractAction) 554 != null 555 && mExtractView.findViewById( 556 android.R.id.inputExtractAccessories) != null 557 && mExtractView.findViewById( 558 android.R.id.inputExtractEditText) != null; 559 } 560 case "verifyGetDisplay": 561 try { 562 return verifyGetDisplay(); 563 } catch (UnsupportedOperationException e) { 564 return e; 565 } 566 case "verifyIsUiContext": 567 return verifyIsUiContext(); 568 case "verifyGetWindowManager": { 569 final WindowManager imsWm = getSystemService(WindowManager.class); 570 final WindowManager configContextWm = 571 configContext.getSystemService(WindowManager.class); 572 final WindowManager attrContextWm = 573 attrContext.getSystemService(WindowManager.class); 574 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 575 } 576 case "verifyGetViewConfiguration": { 577 final ViewConfiguration imsViewConfig = ViewConfiguration.get(this); 578 final ViewConfiguration configContextViewConfig = 579 ViewConfiguration.get(configContext); 580 final ViewConfiguration attrContextViewConfig = 581 ViewConfiguration.get(attrContext); 582 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 583 } 584 case "verifyGetGestureDetector": { 585 GestureDetector.SimpleOnGestureListener listener = 586 new GestureDetector.SimpleOnGestureListener(); 587 final GestureDetector imsGestureDetector = 588 new GestureDetector(this, listener); 589 final GestureDetector configContextGestureDetector = 590 new GestureDetector(configContext, listener); 591 final GestureDetector attrGestureDetector = 592 new GestureDetector(attrContext, listener); 593 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 594 } 595 case "verifyGetWindowManagerOnDisplayContext": { 596 // Obtaining a WindowManager on a display context must throw a strict mode 597 // violation. 598 final WindowManager wm = displayContext 599 .getSystemService(WindowManager.class); 600 601 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 602 } 603 case "verifyGetViewConfigurationOnDisplayContext": { 604 // Obtaining a ViewConfiguration on a display context must throw a strict 605 // mode violation. 606 final ViewConfiguration viewConfiguration = 607 ViewConfiguration.get(displayContext); 608 609 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 610 } 611 case "verifyGetGestureDetectorOnDisplayContext": { 612 // Obtaining a GestureDetector on a display context must throw a strict mode 613 // violation. 614 GestureDetector.SimpleOnGestureListener listener = 615 new GestureDetector.SimpleOnGestureListener(); 616 final GestureDetector gestureDetector = 617 new GestureDetector(displayContext, listener); 618 619 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 620 } 621 case "getStylusHandwritingWindowVisibility": { 622 if (getStylusHandwritingWindow() == null) { 623 return false; 624 } 625 View decorView = getStylusHandwritingWindow().getDecorView(); 626 return decorView != null && decorView.isAttachedToWindow() 627 && decorView.getVisibility() == View.VISIBLE; 628 } 629 case "hasStylusHandwritingWindow": { 630 return getStylusHandwritingWindow() != null; 631 } 632 case "setStylusHandwritingInkView": { 633 View inkView = new View(attrContext); 634 getStylusHandwritingWindow().setContentView(inkView); 635 mEvents = new ArrayList<>(); 636 inkView.setOnTouchListener((view, event) -> 637 mEvents.add(MotionEvent.obtain(event))); 638 return true; 639 } 640 case "setStylusHandwritingTimeout": { 641 setStylusHandwritingSessionTimeout( 642 Duration.ofMillis(command.getExtras().getLong("timeoutMs"))); 643 return true; 644 } 645 case "getStylusHandwritingTimeout": { 646 return getStylusHandwritingSessionTimeout().toMillis(); 647 } 648 case "getStylusHandwritingEvents": { 649 return mEvents; 650 } 651 case "finishStylusHandwriting": { 652 finishStylusHandwriting(); 653 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 654 } 655 case "getCurrentWindowMetricsBounds": { 656 return getSystemService(WindowManager.class) 657 .getCurrentWindowMetrics().getBounds(); 658 } 659 case "setImeCaptionBarVisible": { 660 final boolean visible = command.getExtras().getBoolean("visible"); 661 if (visible) { 662 mView.getRootView().getWindowInsetsController().show(captionBar()); 663 } else { 664 mView.getRootView().getWindowInsetsController().hide(captionBar()); 665 } 666 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 667 } 668 } 669 } 670 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 671 }); 672 } 673 verifyGetDisplay()674 private boolean verifyGetDisplay() throws UnsupportedOperationException { 675 final Display display; 676 final Display configContextDisplay; 677 final Configuration config = new Configuration(); 678 config.setToDefaults(); 679 final Context configContext = createConfigurationContext(config); 680 display = getDisplay(); 681 configContextDisplay = configContext.getDisplay(); 682 return display != null && configContextDisplay != null; 683 } 684 verifyIsUiContext()685 private boolean verifyIsUiContext() { 686 final Configuration config = new Configuration(); 687 config.setToDefaults(); 688 final Context configContext = createConfigurationContext(config); 689 // The value must be true because ConfigurationContext is derived from InputMethodService, 690 // which is a UI Context. 691 final boolean imeDerivedConfigContext = configContext.isUiContext(); 692 // The value must be false because DisplayContext won't receive any config update from 693 // server. 694 final boolean imeDerivedDisplayContext = createDisplayContext(getDisplay()).isUiContext(); 695 return isUiContext() && imeDerivedConfigContext && !imeDerivedDisplayContext; 696 } 697 698 @Nullable 699 private Bundle mInlineSuggestionsExtras; 700 701 @Nullable 702 private CommandReceiver mCommandReceiver; 703 704 @Nullable 705 private ImeSettings mSettings; 706 707 private final AtomicReference<String> mImeEventActionName = new AtomicReference<>(); 708 709 @Nullable getImeEventActionName()710 String getImeEventActionName() { 711 return mImeEventActionName.get(); 712 } 713 714 private final AtomicReference<String> mClientPackageName = new AtomicReference<>(); 715 716 @Nullable getClientPackageName()717 String getClientPackageName() { 718 return mClientPackageName.get(); 719 } 720 721 private boolean mDestroying; 722 723 /** 724 * {@code true} if {@link MockInputMethodImpl#createSession(InputMethod.SessionCallback)} 725 * needs to be suspended. 726 * 727 * <p>Must be touched from the main thread.</p> 728 */ 729 private boolean mSuspendCreateSession = false; 730 731 /** 732 * The suspended {@link MockInputMethodImpl#createSession(InputMethod.SessionCallback)} callback 733 * operation. 734 * 735 * <p>Must be touched from the main thread.</p> 736 */ 737 @Nullable 738 Runnable mSuspendedCreateSession; 739 740 private class MockInputMethodImpl extends InputMethodImpl { 741 @Override showSoftInput(int flags, ResultReceiver resultReceiver)742 public void showSoftInput(int flags, ResultReceiver resultReceiver) { 743 getTracer().showSoftInput(flags, resultReceiver, 744 () -> super.showSoftInput(flags, resultReceiver)); 745 } 746 747 @Override hideSoftInput(int flags, ResultReceiver resultReceiver)748 public void hideSoftInput(int flags, ResultReceiver resultReceiver) { 749 getTracer().hideSoftInput(flags, resultReceiver, 750 () -> super.hideSoftInput(flags, resultReceiver)); 751 } 752 753 @Override createSession(SessionCallback callback)754 public void createSession(SessionCallback callback) { 755 getTracer().createSession(() -> { 756 if (mSuspendCreateSession) { 757 if (mSuspendedCreateSession != null) { 758 throw new IllegalStateException("suspendCreateSession() must be " 759 + "called before receiving another createSession()"); 760 } 761 mSuspendedCreateSession = () -> { 762 if (!mDestroying) { 763 super.createSession(callback); 764 } 765 }; 766 } else { 767 super.createSession(callback); 768 } 769 }); 770 } 771 772 @Override attachToken(IBinder token)773 public void attachToken(IBinder token) { 774 getTracer().attachToken(token, () -> super.attachToken(token)); 775 } 776 777 @Override bindInput(InputBinding binding)778 public void bindInput(InputBinding binding) { 779 getTracer().bindInput(binding, () -> super.bindInput(binding)); 780 } 781 782 @Override unbindInput()783 public void unbindInput() { 784 getTracer().unbindInput(() -> super.unbindInput()); 785 } 786 } 787 788 @Override onCreate()789 public void onCreate() { 790 // Initialize minimum settings to send events in Tracer#onCreate(). 791 mSettings = SettingsProvider.getSettings(); 792 if (mSettings == null) { 793 throw new IllegalStateException("Settings file is not found. " 794 + "Make sure MockImeSession.create() is used to launch Mock IME."); 795 } 796 mClientPackageName.set(mSettings.getClientPackageName()); 797 mImeEventActionName.set(mSettings.getEventCallbackActionName()); 798 799 // TODO(b/159593676): consider to detect more violations 800 if (mSettings.isStrictModeEnabled()) { 801 StrictMode.setVmPolicy( 802 new StrictMode.VmPolicy.Builder() 803 .detectIncorrectContextUse() 804 .penaltyLog() 805 .penaltyListener(Runnable::run, 806 v -> getTracer().onStrictModeViolated(() -> { })) 807 .build()); 808 } 809 810 if (mSettings.isOnBackCallbackEnabled()) { 811 getApplicationInfo().setEnableOnBackInvokedCallback(true); 812 } 813 814 getTracer().onCreate(() -> { 815 super.onCreate(); 816 mHandlerThread.start(); 817 final String actionName = getCommandActionName(mSettings.getEventCallbackActionName()); 818 mCommandReceiver = new CommandReceiver(actionName, this::onReceiveCommand); 819 final IntentFilter filter = new IntentFilter(actionName); 820 final Handler handler = new Handler(mHandlerThread.getLooper()); 821 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 822 registerReceiver(mCommandReceiver, filter, null /* broadcastPermission */, handler, 823 Context.RECEIVER_VISIBLE_TO_INSTANT_APPS | Context.RECEIVER_EXPORTED); 824 } else { 825 registerReceiver(mCommandReceiver, filter, null /* broadcastPermission */, handler); 826 } 827 // If Extension components are not loaded successfully, notify Test app. 828 if (mSettings.isWindowLayoutInfoCallbackEnabled()) { 829 getTracer().onVerify("windowLayoutComponentLoaded", 830 () -> mWindowLayoutComponent != null); 831 } 832 if (mSettings.isVerifyContextApisInOnCreate()) { 833 getTracer().onVerify("isUiContext", this::verifyIsUiContext); 834 getTracer().onVerify("getDisplay", this::verifyGetDisplay); 835 } 836 final int windowFlags = mSettings.getWindowFlags(0); 837 final int windowFlagsMask = mSettings.getWindowFlagsMask(0); 838 if (windowFlags != 0 || windowFlagsMask != 0) { 839 final int prevFlags = getWindow().getWindow().getAttributes().flags; 840 getWindow().getWindow().setFlags(windowFlags, windowFlagsMask); 841 // For some reasons, seems that we need to post another requestLayout() when 842 // FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS bit is changed. 843 // TODO: Investigate the reason. 844 if ((windowFlagsMask & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0) { 845 final boolean hadFlag = (prevFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0; 846 final boolean hasFlag = (windowFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0; 847 if (hadFlag != hasFlag) { 848 final View decorView = getWindow().getWindow().getDecorView(); 849 decorView.post(() -> decorView.requestLayout()); 850 } 851 } 852 } 853 854 // Ensuring bar contrast interferes with the tests. 855 getWindow().getWindow().setStatusBarContrastEnforced(false); 856 getWindow().getWindow().setNavigationBarContrastEnforced(false); 857 858 if (mSettings.hasNavigationBarColor()) { 859 getWindow().getWindow().setNavigationBarColor(mSettings.getNavigationBarColor()); 860 } 861 862 // Initialize to current Configuration to prevent unexpected configDiff value dispatched 863 // in IME event. 864 mLastDispatchedConfiguration.setTo(getResources().getConfiguration()); 865 }); 866 867 if (mSettings.isWindowLayoutInfoCallbackEnabled() && mWindowLayoutComponent != null) { 868 mWindowLayoutInfoConsumer = (windowLayoutInfo) -> getTracer().getWindowLayoutInfo( 869 windowLayoutInfo, () -> {}); 870 mWindowLayoutComponent.addWindowLayoutInfoListener(this, mWindowLayoutInfoConsumer); 871 } 872 } 873 874 @Override onCurrentInputMethodSubtypeChanged(InputMethodSubtype newSubtype)875 protected void onCurrentInputMethodSubtypeChanged(InputMethodSubtype newSubtype) { 876 getTracer().onCurrentInputMethodSubtypeChanged(newSubtype, 877 () -> super.onCurrentInputMethodSubtypeChanged(newSubtype)); 878 } 879 880 @Override onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly)881 public void onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly) { 882 getTracer().onConfigureWindow(win, isFullscreen, isCandidatesOnly, 883 () -> super.onConfigureWindow(win, isFullscreen, isCandidatesOnly)); 884 } 885 886 @Override onEvaluateFullscreenMode()887 public boolean onEvaluateFullscreenMode() { 888 return getTracer().onEvaluateFullscreenMode(() -> { 889 final int policy = mSettings.fullscreenModePolicy(); 890 switch (policy) { 891 case ImeSettings.FullscreenModePolicy.NO_FULLSCREEN: 892 return false; 893 case ImeSettings.FullscreenModePolicy.FORCE_FULLSCREEN: 894 return true; 895 case ImeSettings.FullscreenModePolicy.OS_DEFAULT: 896 return super.onEvaluateFullscreenMode(); 897 default: 898 Log.e(TAG, "unknown FullscreenModePolicy=" + policy); 899 return false; 900 } 901 }); 902 } 903 904 @Override onCreateExtractTextView()905 public View onCreateExtractTextView() { 906 mExtractView = super.onCreateExtractTextView(); 907 return mExtractView; 908 } 909 910 private static final class KeyboardLayoutView extends LinearLayout { 911 @NonNull 912 private final MockIme mMockIme; 913 @NonNull 914 private final ImeSettings mSettings; 915 @NonNull 916 private final View.OnLayoutChangeListener mLayoutListener; 917 918 private final LinearLayout mLayout; 919 920 @Nullable 921 private final LinearLayout mSuggestionView; 922 923 private boolean mDrawsBehindNavBar = false; 924 KeyboardLayoutView(MockIme mockIme, @NonNull ImeSettings imeSettings, @Nullable Consumer<ImeLayoutInfo> onInputViewLayoutChangedCallback)925 KeyboardLayoutView(MockIme mockIme, @NonNull ImeSettings imeSettings, 926 @Nullable Consumer<ImeLayoutInfo> onInputViewLayoutChangedCallback) { 927 super(mockIme); 928 929 mMockIme = mockIme; 930 mSettings = imeSettings; 931 932 setOrientation(VERTICAL); 933 934 final int defaultBackgroundColor = 935 getResources().getColor(android.R.color.holo_orange_dark, null); 936 937 final int mainSpacerHeight = mSettings.getInputViewHeight(LayoutParams.WRAP_CONTENT); 938 mLayout = new LinearLayout(getContext()); 939 mLayout.setOrientation(LinearLayout.VERTICAL); 940 941 if (mSettings.getInlineSuggestionsEnabled()) { 942 final HorizontalScrollView scrollView = new HorizontalScrollView(getContext()); 943 final LayoutParams scrollViewParams = new LayoutParams(MATCH_PARENT, 100); 944 scrollView.setLayoutParams(scrollViewParams); 945 946 final LinearLayout suggestionView = new LinearLayout(getContext()); 947 suggestionView.setBackgroundColor(0xFFEEEEEE); 948 final String suggestionViewContentDesc = 949 mSettings.getInlineSuggestionViewContentDesc(null /* default */); 950 if (suggestionViewContentDesc != null) { 951 suggestionView.setContentDescription(suggestionViewContentDesc); 952 } 953 scrollView.addView(suggestionView, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); 954 mSuggestionView = suggestionView; 955 956 mLayout.addView(scrollView); 957 } else { 958 mSuggestionView = null; 959 } 960 961 { 962 final FrameLayout secondaryLayout = new FrameLayout(getContext()); 963 secondaryLayout.setForegroundGravity(Gravity.CENTER); 964 965 final TextView textView = new TextView(getContext()); 966 textView.setLayoutParams(new LayoutParams(MATCH_PARENT, WRAP_CONTENT)); 967 textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); 968 textView.setGravity(Gravity.CENTER); 969 textView.setText(getImeId()); 970 textView.setBackgroundColor( 971 mSettings.getBackgroundColor(defaultBackgroundColor)); 972 secondaryLayout.addView(textView); 973 974 if (mSettings.isWatermarkEnabled(true /* defaultValue */)) { 975 final ImageView imageView = new ImageView(getContext()); 976 final Bitmap bitmap = Watermark.create(); 977 imageView.setImageBitmap(bitmap); 978 secondaryLayout.addView(imageView, 979 new FrameLayout.LayoutParams(bitmap.getWidth(), bitmap.getHeight(), 980 mSettings.getWatermarkGravity(Gravity.CENTER))); 981 } 982 983 mLayout.addView(secondaryLayout); 984 } 985 986 addView(mLayout, MATCH_PARENT, mainSpacerHeight); 987 988 final int systemUiVisibility = mSettings.getInputViewSystemUiVisibility(0); 989 if (systemUiVisibility != 0) { 990 setSystemUiVisibility(systemUiVisibility); 991 } 992 993 if (mSettings.getDrawsBehindNavBar()) { 994 mDrawsBehindNavBar = true; 995 mMockIme.getWindow().getWindow().setDecorFitsSystemWindows(false); 996 setSystemUiVisibility(getSystemUiVisibility() 997 | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); 998 } 999 1000 mLayoutListener = (View v, int left, int top, int right, int bottom, int oldLeft, 1001 int oldTop, int oldRight, int oldBottom) -> 1002 onInputViewLayoutChangedCallback.accept( 1003 ImeLayoutInfo.fromLayoutListenerCallback( 1004 v, left, top, right, bottom, oldLeft, oldTop, oldRight, 1005 oldBottom)); 1006 this.addOnLayoutChangeListener(mLayoutListener); 1007 } 1008 setHeight(int height)1009 private void setHeight(int height) { 1010 mLayout.getLayoutParams().height = height; 1011 mLayout.requestLayout(); 1012 } 1013 updateBottomPaddingIfNecessary(int newPaddingBottom)1014 private void updateBottomPaddingIfNecessary(int newPaddingBottom) { 1015 if (getPaddingBottom() != newPaddingBottom) { 1016 setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), newPaddingBottom); 1017 } 1018 } 1019 1020 @Override onApplyWindowInsets(WindowInsets insets)1021 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 1022 if (insets.isConsumed() 1023 || mDrawsBehindNavBar 1024 || (getSystemUiVisibility() & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0) { 1025 // In this case we are not interested in consuming NavBar region. 1026 // Make sure that the bottom padding is empty. 1027 updateBottomPaddingIfNecessary(0); 1028 return insets; 1029 } 1030 1031 // In some cases the bottom system window inset is not a navigation bar. Wear devices 1032 // that have bottom chin are examples. For now, assume that it's a navigation bar if it 1033 // has the same height as the root window's stable bottom inset. 1034 final WindowInsets rootWindowInsets = getRootWindowInsets(); 1035 if (rootWindowInsets != null && (rootWindowInsets.getStableInsetBottom() 1036 != insets.getSystemWindowInsetBottom())) { 1037 // This is probably not a NavBar. 1038 updateBottomPaddingIfNecessary(0); 1039 return insets; 1040 } 1041 1042 final int possibleNavBarHeight = insets.getSystemWindowInsetBottom(); 1043 updateBottomPaddingIfNecessary(possibleNavBarHeight); 1044 return possibleNavBarHeight <= 0 1045 ? insets 1046 : insets.replaceSystemWindowInsets( 1047 insets.getSystemWindowInsetLeft(), 1048 insets.getSystemWindowInsetTop(), 1049 insets.getSystemWindowInsetRight(), 1050 0 /* bottom */); 1051 } 1052 1053 @Override onWindowVisibilityChanged(int visibility)1054 protected void onWindowVisibilityChanged(int visibility) { 1055 mMockIme.getTracer().onWindowVisibilityChanged(() -> { 1056 super.onWindowVisibilityChanged(visibility); 1057 }, visibility); 1058 } 1059 1060 @Override onDetachedFromWindow()1061 protected void onDetachedFromWindow() { 1062 super.onDetachedFromWindow(); 1063 removeOnLayoutChangeListener(mLayoutListener); 1064 } 1065 1066 @MainThread updateInlineSuggestions( @onNull PendingInlineSuggestions pendingInlineSuggestions)1067 private void updateInlineSuggestions( 1068 @NonNull PendingInlineSuggestions pendingInlineSuggestions) { 1069 Log.d(TAG, "updateInlineSuggestions() called: " + pendingInlineSuggestions.mTotalCount); 1070 if (mSuggestionView == null || !pendingInlineSuggestions.mValid.get()) { 1071 return; 1072 } 1073 mSuggestionView.removeAllViews(); 1074 for (int i = 0; i < pendingInlineSuggestions.mTotalCount; i++) { 1075 View view = pendingInlineSuggestions.mViews[i]; 1076 if (view == null) { 1077 continue; 1078 } 1079 mSuggestionView.addView(view); 1080 } 1081 } 1082 } 1083 1084 KeyboardLayoutView mView; 1085 onInputViewLayoutChanged(@onNull ImeLayoutInfo layoutInfo)1086 private void onInputViewLayoutChanged(@NonNull ImeLayoutInfo layoutInfo) { 1087 getTracer().onInputViewLayoutChanged(layoutInfo, () -> { }); 1088 } 1089 1090 @Override onCreateInputView()1091 public View onCreateInputView() { 1092 return getTracer().onCreateInputView(() -> { 1093 mView = new KeyboardLayoutView(this, mSettings, this::onInputViewLayoutChanged); 1094 return mView; 1095 }); 1096 } 1097 1098 @Override 1099 public void onStartInput(EditorInfo editorInfo, boolean restarting) { 1100 getTracer().onStartInput(editorInfo, restarting, 1101 () -> super.onStartInput(editorInfo, restarting)); 1102 } 1103 1104 @Override 1105 public void onStartInputView(EditorInfo editorInfo, boolean restarting) { 1106 getTracer().onStartInputView(editorInfo, restarting, 1107 () -> super.onStartInputView(editorInfo, restarting)); 1108 } 1109 1110 1111 @Override 1112 public void onPrepareStylusHandwriting() { 1113 getTracer().onPrepareStylusHandwriting(() -> super.onPrepareStylusHandwriting()); 1114 } 1115 1116 @Override 1117 public boolean onStartStylusHandwriting() { 1118 if (mEvents != null) { 1119 mEvents.clear(); 1120 } 1121 getTracer().onStartStylusHandwriting(() -> super.onStartStylusHandwriting()); 1122 return true; 1123 } 1124 1125 @Override 1126 public void onStylusHandwritingMotionEvent(@NonNull MotionEvent motionEvent) { 1127 if (mEvents == null) { 1128 mEvents = new ArrayList<>(); 1129 } 1130 mEvents.add(MotionEvent.obtain(motionEvent)); 1131 getTracer().onStylusHandwritingMotionEvent(() 1132 -> super.onStylusHandwritingMotionEvent(motionEvent)); 1133 } 1134 1135 @Override 1136 public void onUpdateEditorToolType(int toolType) { 1137 if (mEvents != null) { 1138 mEvents.clear(); 1139 } 1140 getTracer().onUpdateEditorToolType(toolType, () -> super.onUpdateEditorToolType(toolType)); 1141 } 1142 1143 @Override 1144 public void onFinishStylusHandwriting() { 1145 getTracer().onFinishStylusHandwriting(() -> super.onFinishStylusHandwriting()); 1146 } 1147 1148 1149 @Override 1150 public void onFinishInputView(boolean finishingInput) { 1151 getTracer().onFinishInputView(finishingInput, 1152 () -> super.onFinishInputView(finishingInput)); 1153 } 1154 1155 @Override 1156 public void onFinishInput() { 1157 getTracer().onFinishInput(() -> super.onFinishInput()); 1158 } 1159 1160 @Override 1161 public boolean onKeyDown(int keyCode, KeyEvent event) { 1162 return getTracer().onKeyDown(keyCode, event, () -> super.onKeyDown(keyCode, event)); 1163 } 1164 1165 @Override 1166 public void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) { 1167 getTracer().onUpdateCursorAnchorInfo(cursorAnchorInfo, 1168 () -> super.onUpdateCursorAnchorInfo(cursorAnchorInfo)); 1169 } 1170 1171 @Override 1172 public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, 1173 int candidatesStart, int candidatesEnd) { 1174 getTracer().onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 1175 candidatesStart, candidatesEnd, 1176 () -> super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 1177 candidatesStart, candidatesEnd)); 1178 } 1179 1180 @CallSuper 1181 public boolean onEvaluateInputViewShown() { 1182 return getTracer().onEvaluateInputViewShown(() -> { 1183 // onShowInputRequested() is indeed @CallSuper so we always call this, even when the 1184 // result is ignored. 1185 final boolean originalResult = super.onEvaluateInputViewShown(); 1186 if (!mSettings.getHardKeyboardConfigurationBehaviorAllowed(false)) { 1187 final Configuration config = getResources().getConfiguration(); 1188 if (config.keyboard != Configuration.KEYBOARD_NOKEYS 1189 && config.hardKeyboardHidden != Configuration.HARDKEYBOARDHIDDEN_YES) { 1190 // Override the behavior of InputMethodService#onEvaluateInputViewShown() 1191 return true; 1192 } 1193 } 1194 return originalResult; 1195 }); 1196 } 1197 1198 @Override 1199 public boolean onShowInputRequested(int flags, boolean configChange) { 1200 return getTracer().onShowInputRequested(flags, configChange, () -> { 1201 // onShowInputRequested() is not marked with @CallSuper, but just in case. 1202 final boolean originalResult = super.onShowInputRequested(flags, configChange); 1203 if (!mSettings.getHardKeyboardConfigurationBehaviorAllowed(false)) { 1204 if ((flags & InputMethod.SHOW_EXPLICIT) == 0 1205 && getResources().getConfiguration().keyboard 1206 != Configuration.KEYBOARD_NOKEYS) { 1207 // Override the behavior of InputMethodService#onShowInputRequested() 1208 return true; 1209 } 1210 } 1211 return originalResult; 1212 }); 1213 } 1214 1215 @Override 1216 public void onDestroy() { 1217 getTracer().onDestroy(() -> { 1218 mDestroying = true; 1219 super.onDestroy(); 1220 if (mSettings.isWindowLayoutInfoCallbackEnabled() && mWindowLayoutComponent != null) { 1221 mWindowLayoutComponent.removeWindowLayoutInfoListener(mWindowLayoutInfoConsumer); 1222 } 1223 unregisterReceiver(mCommandReceiver); 1224 mHandlerThread.quitSafely(); 1225 }); 1226 } 1227 1228 @Override 1229 public AbstractInputMethodImpl onCreateInputMethodInterface() { 1230 return getTracer().onCreateInputMethodInterface(() -> new MockInputMethodImpl()); 1231 } 1232 1233 private final ThreadLocal<Tracer> mThreadLocalTracer = new ThreadLocal<>(); 1234 1235 private Tracer getTracer() { 1236 Tracer tracer = mThreadLocalTracer.get(); 1237 if (tracer == null) { 1238 tracer = new Tracer(this); 1239 mThreadLocalTracer.set(tracer); 1240 } 1241 return tracer; 1242 } 1243 1244 @NonNull 1245 private ImeState getState() { 1246 final boolean hasInputBinding = getCurrentInputBinding() != null; 1247 final boolean hasFallbackInputConnection = 1248 !hasInputBinding 1249 || getCurrentInputConnection() == getCurrentInputBinding().getConnection(); 1250 return new ImeState(hasInputBinding, hasFallbackInputConnection); 1251 } 1252 1253 private PendingInlineSuggestions mPendingInlineSuggestions; 1254 1255 private static final class PendingInlineSuggestions { 1256 final InlineSuggestionsResponse mResponse; 1257 final int mTotalCount; 1258 final View[] mViews; 1259 final AtomicInteger mInflatedViewCount; 1260 final AtomicBoolean mValid = new AtomicBoolean(true); 1261 1262 PendingInlineSuggestions(InlineSuggestionsResponse response) { 1263 mResponse = response; 1264 mTotalCount = response.getInlineSuggestions().size(); 1265 mViews = new View[mTotalCount]; 1266 mInflatedViewCount = new AtomicInteger(0); 1267 } 1268 } 1269 1270 @MainThread 1271 @Override 1272 public InlineSuggestionsRequest onCreateInlineSuggestionsRequest(Bundle uiExtras) { 1273 StylesBuilder stylesBuilder = UiVersions.newStylesBuilder(); 1274 stylesBuilder.addStyle(InlineSuggestionUi.newStyleBuilder().build()); 1275 Bundle styles = stylesBuilder.build(); 1276 1277 final boolean supportedInlineSuggestions; 1278 if (mInlineSuggestionsExtras != null) { 1279 styles.putAll(mInlineSuggestionsExtras); 1280 supportedInlineSuggestions = 1281 mInlineSuggestionsExtras.getBoolean("InlineSuggestions", true); 1282 } else { 1283 supportedInlineSuggestions = true; 1284 } 1285 1286 if (!supportedInlineSuggestions) { 1287 return null; 1288 } 1289 1290 return getTracer().onCreateInlineSuggestionsRequest(() -> { 1291 final ArrayList<InlinePresentationSpec> presentationSpecs = new ArrayList<>(); 1292 presentationSpecs.add(new InlinePresentationSpec.Builder(new Size(100, 100), 1293 new Size(400, 100)).setStyle(styles).build()); 1294 presentationSpecs.add(new InlinePresentationSpec.Builder(new Size(100, 100), 1295 new Size(400, 100)).setStyle(styles).build()); 1296 1297 final InlinePresentationSpec tooltipSpec = 1298 new InlinePresentationSpec.Builder(new Size(100, 100), 1299 new Size(400, 100)).setStyle(styles).build(); 1300 final InlineSuggestionsRequest.Builder builder = 1301 new InlineSuggestionsRequest.Builder(presentationSpecs) 1302 .setInlineTooltipPresentationSpec(tooltipSpec) 1303 .setMaxSuggestionCount(6); 1304 if (mInlineSuggestionsExtras != null) { 1305 builder.setExtras(mInlineSuggestionsExtras.deepCopy()); 1306 } 1307 return builder.build(); 1308 }); 1309 } 1310 1311 @MainThread 1312 @Override 1313 public boolean onInlineSuggestionsResponse(@NonNull InlineSuggestionsResponse response) { 1314 return getTracer().onInlineSuggestionsResponse(response, () -> { 1315 final PendingInlineSuggestions pendingInlineSuggestions = 1316 new PendingInlineSuggestions(response); 1317 if (mPendingInlineSuggestions != null) { 1318 mPendingInlineSuggestions.mValid.set(false); 1319 } 1320 mPendingInlineSuggestions = pendingInlineSuggestions; 1321 if (pendingInlineSuggestions.mTotalCount == 0) { 1322 if (mView != null) { 1323 mView.updateInlineSuggestions(pendingInlineSuggestions); 1324 } 1325 return true; 1326 } 1327 1328 final ExecutorService executorService = Executors.newCachedThreadPool(); 1329 for (int i = 0; i < pendingInlineSuggestions.mTotalCount; i++) { 1330 final int index = i; 1331 InlineSuggestion inlineSuggestion = 1332 pendingInlineSuggestions.mResponse.getInlineSuggestions().get(index); 1333 inlineSuggestion.inflate( 1334 this, 1335 new Size(WRAP_CONTENT, WRAP_CONTENT), 1336 executorService, 1337 suggestionView -> { 1338 Log.d(TAG, "new inline suggestion view ready"); 1339 if (suggestionView != null) { 1340 suggestionView.setOnClickListener((v) -> { 1341 getTracer().onInlineSuggestionClickedEvent(() -> { }); 1342 }); 1343 suggestionView.setOnLongClickListener((v) -> { 1344 getTracer().onInlineSuggestionLongClickedEvent(() -> { }); 1345 return true; 1346 }); 1347 pendingInlineSuggestions.mViews[index] = suggestionView; 1348 } 1349 if (pendingInlineSuggestions.mInflatedViewCount.incrementAndGet() 1350 == pendingInlineSuggestions.mTotalCount 1351 && pendingInlineSuggestions.mValid.get()) { 1352 Log.d(TAG, "ready to display all suggestions"); 1353 mMainHandler.post(() -> 1354 mView.updateInlineSuggestions(pendingInlineSuggestions)); 1355 } 1356 }); 1357 } 1358 return true; 1359 }); 1360 } 1361 1362 @Override 1363 public void onConfigurationChanged(Configuration configuration) { 1364 // Broadcasting configuration change is implemented at WindowProviderService level. 1365 super.onConfigurationChanged(configuration); 1366 getTracer().onConfigurationChanged(() -> {}, configuration); 1367 mLastDispatchedConfiguration.setTo(configuration); 1368 } 1369 1370 /** 1371 * Event tracing helper class for {@link MockIme}. 1372 */ 1373 private static final class Tracer { 1374 1375 @NonNull 1376 private final MockIme mIme; 1377 1378 private final int mThreadId = Process.myTid(); 1379 1380 @NonNull 1381 private final String mThreadName = 1382 Thread.currentThread().getName() != null ? Thread.currentThread().getName() : ""; 1383 1384 private final boolean mIsMainThread = 1385 Looper.getMainLooper().getThread() == Thread.currentThread(); 1386 1387 private int mNestLevel = 0; 1388 1389 private String mImeEventActionName; 1390 1391 private String mClientPackageName; 1392 1393 Tracer(@NonNull MockIme mockIme) { 1394 mIme = mockIme; 1395 } 1396 1397 private void sendEventInternal(@NonNull ImeEvent event) { 1398 if (mImeEventActionName == null) { 1399 mImeEventActionName = mIme.getImeEventActionName(); 1400 } 1401 if (mClientPackageName == null) { 1402 mClientPackageName = mIme.getClientPackageName(); 1403 } 1404 if (mImeEventActionName == null || mClientPackageName == null) { 1405 Log.e(TAG, "Tracer cannot be used before onCreate()"); 1406 return; 1407 } 1408 final Intent intent = new Intent() 1409 .setAction(mImeEventActionName) 1410 .setPackage(mClientPackageName) 1411 .putExtras(event.toBundle()) 1412 .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY 1413 | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS 1414 | Intent.FLAG_RECEIVER_FOREGROUND); 1415 mIme.sendBroadcast(intent); 1416 } 1417 1418 private void recordEventInternal(@NonNull String eventName, @NonNull Runnable runnable) { 1419 recordEventInternal(eventName, runnable, new Bundle()); 1420 } 1421 1422 private void recordEventInternal(@NonNull String eventName, @NonNull Runnable runnable, 1423 @NonNull Bundle arguments) { 1424 recordEventInternal(eventName, () -> { 1425 runnable.run(); return ImeEvent.RETURN_VALUE_UNAVAILABLE; 1426 }, arguments); 1427 } 1428 1429 private <T> T recordEventInternal(@NonNull String eventName, 1430 @NonNull Supplier<T> supplier) { 1431 return recordEventInternal(eventName, supplier, new Bundle()); 1432 } 1433 1434 private <T> T recordEventInternal(@NonNull String eventName, 1435 @NonNull Supplier<T> supplier, @NonNull Bundle arguments) { 1436 final ImeState enterState = mIme.getState(); 1437 final long enterTimestamp = SystemClock.elapsedRealtimeNanos(); 1438 final long enterWallTime = System.currentTimeMillis(); 1439 final int nestLevel = mNestLevel; 1440 // Send enter event 1441 sendEventInternal(new ImeEvent(eventName, nestLevel, mThreadName, 1442 mThreadId, mIsMainThread, enterTimestamp, 0, enterWallTime, 1443 0, enterState, null, arguments, 1444 ImeEvent.RETURN_VALUE_UNAVAILABLE)); 1445 ++mNestLevel; 1446 T result; 1447 try { 1448 result = supplier.get(); 1449 } finally { 1450 --mNestLevel; 1451 } 1452 final long exitTimestamp = SystemClock.elapsedRealtimeNanos(); 1453 final long exitWallTime = System.currentTimeMillis(); 1454 final ImeState exitState = mIme.getState(); 1455 // Send exit event 1456 sendEventInternal(new ImeEvent(eventName, nestLevel, mThreadName, 1457 mThreadId, mIsMainThread, enterTimestamp, exitTimestamp, enterWallTime, 1458 exitWallTime, enterState, exitState, arguments, result)); 1459 return result; 1460 } 1461 1462 void onCreate(@NonNull Runnable runnable) { 1463 recordEventInternal("onCreate", runnable); 1464 } 1465 1466 void createSession(@NonNull Runnable runnable) { 1467 recordEventInternal("createSession", runnable); 1468 } 1469 1470 void onVerify(String name, @NonNull BooleanSupplier supplier) { 1471 final Bundle arguments = new Bundle(); 1472 arguments.putString("name", name); 1473 recordEventInternal("onVerify", supplier::getAsBoolean, arguments); 1474 } 1475 1476 void onCurrentInputMethodSubtypeChanged(InputMethodSubtype newSubtype, 1477 @NonNull Runnable runnable) { 1478 final Bundle arguments = new Bundle(); 1479 arguments.putParcelable("newSubtype", newSubtype); 1480 recordEventInternal("onCurrentInputMethodSubtypeChanged", runnable, arguments); 1481 } 1482 1483 void onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly, 1484 @NonNull Runnable runnable) { 1485 final Bundle arguments = new Bundle(); 1486 arguments.putBoolean("isFullscreen", isFullscreen); 1487 arguments.putBoolean("isCandidatesOnly", isCandidatesOnly); 1488 recordEventInternal("onConfigureWindow", runnable, arguments); 1489 } 1490 1491 boolean onEvaluateFullscreenMode(@NonNull BooleanSupplier supplier) { 1492 return recordEventInternal("onEvaluateFullscreenMode", supplier::getAsBoolean); 1493 } 1494 1495 boolean onEvaluateInputViewShown(@NonNull BooleanSupplier supplier) { 1496 return recordEventInternal("onEvaluateInputViewShown", supplier::getAsBoolean); 1497 } 1498 1499 View onCreateInputView(@NonNull Supplier<View> supplier) { 1500 return recordEventInternal("onCreateInputView", supplier); 1501 } 1502 1503 void onStartInput(EditorInfo editorInfo, boolean restarting, @NonNull Runnable runnable) { 1504 final Bundle arguments = new Bundle(); 1505 arguments.putParcelable("editorInfo", editorInfo); 1506 arguments.putBoolean("restarting", restarting); 1507 recordEventInternal("onStartInput", runnable, arguments); 1508 } 1509 1510 void onWindowVisibilityChanged(@NonNull Runnable runnable, int visibility) { 1511 final Bundle arguments = new Bundle(); 1512 arguments.putInt("visible", visibility); 1513 recordEventInternal("onWindowVisibilityChanged", runnable, arguments); 1514 } 1515 1516 void onStartInputView(EditorInfo editorInfo, boolean restarting, 1517 @NonNull Runnable runnable) { 1518 final Bundle arguments = new Bundle(); 1519 arguments.putParcelable("editorInfo", editorInfo); 1520 arguments.putBoolean("restarting", restarting); 1521 recordEventInternal("onStartInputView", runnable, arguments); 1522 } 1523 1524 void onPrepareStylusHandwriting(@NonNull Runnable runnable) { 1525 final Bundle arguments = new Bundle(); 1526 arguments.putParcelable("editorInfo", mIme.getCurrentInputEditorInfo()); 1527 recordEventInternal("onPrepareStylusHandwriting", runnable, arguments); 1528 } 1529 1530 void onStartStylusHandwriting(@NonNull Runnable runnable) { 1531 final Bundle arguments = new Bundle(); 1532 arguments.putParcelable("editorInfo", mIme.getCurrentInputEditorInfo()); 1533 recordEventInternal("onStartStylusHandwriting", runnable, arguments); 1534 } 1535 1536 void onStylusHandwritingMotionEvent(@NonNull Runnable runnable) { 1537 recordEventInternal("onStylusMotionEvent", runnable); 1538 } 1539 1540 void onFinishStylusHandwriting(@NonNull Runnable runnable) { 1541 final Bundle arguments = new Bundle(); 1542 arguments.putParcelable("editorInfo", mIme.getCurrentInputEditorInfo()); 1543 recordEventInternal("onFinishStylusHandwriting", runnable, arguments); 1544 } 1545 1546 void onFinishInputView(boolean finishingInput, @NonNull Runnable runnable) { 1547 final Bundle arguments = new Bundle(); 1548 arguments.putBoolean("finishingInput", finishingInput); 1549 recordEventInternal("onFinishInputView", runnable, arguments); 1550 } 1551 1552 void onFinishInput(@NonNull Runnable runnable) { 1553 recordEventInternal("onFinishInput", runnable); 1554 } 1555 1556 void onUpdateEditorToolType(int toolType, @NonNull Runnable runnable) { 1557 final Bundle arguments = new Bundle(); 1558 arguments.putInt("toolType", toolType); 1559 recordEventInternal("onUpdateEditorToolType", runnable, arguments); 1560 } 1561 1562 boolean onKeyDown(int keyCode, KeyEvent event, @NonNull BooleanSupplier supplier) { 1563 final Bundle arguments = new Bundle(); 1564 arguments.putInt("keyCode", keyCode); 1565 arguments.putParcelable("event", event); 1566 return recordEventInternal("onKeyDown", supplier::getAsBoolean, arguments); 1567 } 1568 1569 void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo, 1570 @NonNull Runnable runnable) { 1571 final Bundle arguments = new Bundle(); 1572 arguments.putParcelable("cursorAnchorInfo", cursorAnchorInfo); 1573 recordEventInternal("onUpdateCursorAnchorInfo", runnable, arguments); 1574 } 1575 1576 void onUpdateSelection(int oldSelStart, 1577 int oldSelEnd, 1578 int newSelStart, 1579 int newSelEnd, 1580 int candidatesStart, 1581 int candidatesEnd, 1582 @NonNull Runnable runnable) { 1583 final Bundle arguments = new Bundle(); 1584 arguments.putInt("oldSelStart", oldSelStart); 1585 arguments.putInt("oldSelEnd", oldSelEnd); 1586 arguments.putInt("newSelStart", newSelStart); 1587 arguments.putInt("newSelEnd", newSelEnd); 1588 arguments.putInt("candidatesStart", candidatesStart); 1589 arguments.putInt("candidatesEnd", candidatesEnd); 1590 recordEventInternal("onUpdateSelection", runnable, arguments); 1591 } 1592 1593 boolean onShowInputRequested(int flags, boolean configChange, 1594 @NonNull BooleanSupplier supplier) { 1595 final Bundle arguments = new Bundle(); 1596 arguments.putInt("flags", flags); 1597 arguments.putBoolean("configChange", configChange); 1598 return recordEventInternal("onShowInputRequested", supplier::getAsBoolean, arguments); 1599 } 1600 1601 void onDestroy(@NonNull Runnable runnable) { 1602 recordEventInternal("onDestroy", runnable); 1603 } 1604 1605 void attachToken(IBinder token, @NonNull Runnable runnable) { 1606 final Bundle arguments = new Bundle(); 1607 arguments.putBinder("token", token); 1608 recordEventInternal("attachToken", runnable, arguments); 1609 } 1610 1611 void bindInput(InputBinding binding, @NonNull Runnable runnable) { 1612 final Bundle arguments = new Bundle(); 1613 arguments.putParcelable("binding", binding); 1614 recordEventInternal("bindInput", runnable, arguments); 1615 } 1616 1617 void unbindInput(@NonNull Runnable runnable) { 1618 recordEventInternal("unbindInput", runnable); 1619 } 1620 1621 void showSoftInput(int flags, ResultReceiver resultReceiver, @NonNull Runnable runnable) { 1622 final Bundle arguments = new Bundle(); 1623 arguments.putInt("flags", flags); 1624 arguments.putParcelable("resultReceiver", resultReceiver); 1625 recordEventInternal("showSoftInput", runnable, arguments); 1626 } 1627 1628 void hideSoftInput(int flags, ResultReceiver resultReceiver, @NonNull Runnable runnable) { 1629 final Bundle arguments = new Bundle(); 1630 arguments.putInt("flags", flags); 1631 arguments.putParcelable("resultReceiver", resultReceiver); 1632 recordEventInternal("hideSoftInput", runnable, arguments); 1633 } 1634 1635 AbstractInputMethodImpl onCreateInputMethodInterface( 1636 @NonNull Supplier<AbstractInputMethodImpl> supplier) { 1637 return recordEventInternal("onCreateInputMethodInterface", supplier); 1638 } 1639 1640 void onReceiveCommand(@NonNull ImeCommand command, @NonNull Runnable runnable) { 1641 final Bundle arguments = new Bundle(); 1642 arguments.putBundle("command", command.toBundle()); 1643 recordEventInternal("onReceiveCommand", runnable, arguments); 1644 } 1645 1646 void onHandleCommand( 1647 @NonNull ImeCommand command, @NonNull Supplier<Object> resultSupplier) { 1648 final Bundle arguments = new Bundle(); 1649 arguments.putBundle("command", command.toBundle()); 1650 recordEventInternal("onHandleCommand", resultSupplier, arguments); 1651 } 1652 1653 void onInputViewLayoutChanged(@NonNull ImeLayoutInfo imeLayoutInfo, 1654 @NonNull Runnable runnable) { 1655 final Bundle arguments = new Bundle(); 1656 imeLayoutInfo.writeToBundle(arguments); 1657 recordEventInternal("onInputViewLayoutChanged", runnable, arguments); 1658 } 1659 1660 void onStrictModeViolated(@NonNull Runnable runnable) { 1661 final Bundle arguments = new Bundle(); 1662 recordEventInternal("onStrictModeViolated", runnable, arguments); 1663 } 1664 1665 InlineSuggestionsRequest onCreateInlineSuggestionsRequest( 1666 @NonNull Supplier<InlineSuggestionsRequest> supplier) { 1667 return recordEventInternal("onCreateInlineSuggestionsRequest", supplier); 1668 } 1669 1670 boolean onInlineSuggestionsResponse(@NonNull InlineSuggestionsResponse response, 1671 @NonNull BooleanSupplier supplier) { 1672 final Bundle arguments = new Bundle(); 1673 arguments.putParcelable("response", response); 1674 return recordEventInternal("onInlineSuggestionsResponse", supplier::getAsBoolean, 1675 arguments); 1676 } 1677 1678 void onInlineSuggestionClickedEvent(@NonNull Runnable runnable) { 1679 final Bundle arguments = new Bundle(); 1680 recordEventInternal("onInlineSuggestionClickedEvent", runnable, arguments); 1681 } 1682 1683 void onInlineSuggestionLongClickedEvent(@NonNull Runnable runnable) { 1684 final Bundle arguments = new Bundle(); 1685 recordEventInternal("onInlineSuggestionLongClickedEvent", runnable, arguments); 1686 } 1687 1688 void onConfigurationChanged(@NonNull Runnable runnable, Configuration configuration) { 1689 final Bundle arguments = new Bundle(); 1690 arguments.putParcelable("Configuration", configuration); 1691 arguments.putInt("ConfigUpdates", configuration.diff( 1692 mIme.mLastDispatchedConfiguration)); 1693 recordEventInternal("onConfigurationChanged", runnable, arguments); 1694 } 1695 1696 void onPerformHandwritingGestureResult(int result, long requestId, Runnable runnable) { 1697 final Bundle arguments = new Bundle(); 1698 arguments.putInt("result", result); 1699 arguments.putLong("requestId", requestId); 1700 recordEventInternal("onPerformHandwritingGestureResult", runnable, arguments); 1701 } 1702 1703 public void onRequestTextBoundsInfoResult(TextBoundsInfoResult result, long requestId) { 1704 final Bundle arguments = new Bundle(); 1705 arguments.putInt("resultCode", result.getResultCode()); 1706 arguments.putParcelable("boundsInfo", result.getTextBoundsInfo()); 1707 arguments.putLong("requestId", requestId); 1708 recordEventInternal("onRequestTextBoundsInfoResult", () -> {}, arguments); 1709 } 1710 1711 void getWindowLayoutInfo(@NonNull WindowLayoutInfo windowLayoutInfo, 1712 @NonNull Runnable runnable) { 1713 final Bundle arguments = new Bundle(); 1714 ImeEventStreamTestUtils.WindowLayoutInfoParcelable parcel = 1715 new ImeEventStreamTestUtils.WindowLayoutInfoParcelable(windowLayoutInfo); 1716 arguments.putParcelable("WindowLayoutInfo", parcel); 1717 recordEventInternal("getWindowLayoutInfo", runnable, arguments); 1718 } 1719 } 1720 } 1721