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