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.view.ViewGroup.LayoutParams.MATCH_PARENT; 20 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 21 import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; 22 23 import android.content.BroadcastReceiver; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.content.pm.PackageManager; 29 import android.content.res.Configuration; 30 import android.graphics.Bitmap; 31 import android.inputmethodservice.InputMethodService; 32 import android.os.Build; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.HandlerThread; 36 import android.os.IBinder; 37 import android.os.Looper; 38 import android.os.Process; 39 import android.os.ResultReceiver; 40 import android.os.StrictMode; 41 import android.os.SystemClock; 42 import android.text.TextUtils; 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.View; 51 import android.view.ViewConfiguration; 52 import android.view.Window; 53 import android.view.WindowInsets; 54 import android.view.WindowManager; 55 import android.view.inputmethod.CompletionInfo; 56 import android.view.inputmethod.CorrectionInfo; 57 import android.view.inputmethod.CursorAnchorInfo; 58 import android.view.inputmethod.EditorInfo; 59 import android.view.inputmethod.ExtractedTextRequest; 60 import android.view.inputmethod.InlineSuggestion; 61 import android.view.inputmethod.InlineSuggestionsRequest; 62 import android.view.inputmethod.InlineSuggestionsResponse; 63 import android.view.inputmethod.InputBinding; 64 import android.view.inputmethod.InputConnection; 65 import android.view.inputmethod.InputContentInfo; 66 import android.view.inputmethod.InputMethod; 67 import android.widget.FrameLayout; 68 import android.widget.HorizontalScrollView; 69 import android.widget.ImageView; 70 import android.widget.LinearLayout; 71 import android.widget.TextView; 72 import android.widget.inline.InlinePresentationSpec; 73 74 import androidx.annotation.AnyThread; 75 import androidx.annotation.CallSuper; 76 import androidx.annotation.MainThread; 77 import androidx.annotation.NonNull; 78 import androidx.annotation.Nullable; 79 import androidx.annotation.WorkerThread; 80 import androidx.autofill.inline.UiVersions; 81 import androidx.autofill.inline.UiVersions.StylesBuilder; 82 import androidx.autofill.inline.v1.InlineSuggestionUi; 83 84 import java.util.ArrayList; 85 import java.util.concurrent.ExecutorService; 86 import java.util.concurrent.Executors; 87 import java.util.concurrent.atomic.AtomicBoolean; 88 import java.util.concurrent.atomic.AtomicInteger; 89 import java.util.concurrent.atomic.AtomicReference; 90 import java.util.function.BooleanSupplier; 91 import java.util.function.Consumer; 92 import java.util.function.Supplier; 93 94 /** 95 * Mock IME for end-to-end tests. 96 */ 97 public final class MockIme extends InputMethodService { 98 99 private static final String TAG = "MockIme"; 100 101 private static final String PACKAGE_NAME = "com.android.cts.mockime"; 102 getComponentName()103 static ComponentName getComponentName() { 104 return new ComponentName(PACKAGE_NAME, MockIme.class.getName()); 105 } 106 getImeId()107 static String getImeId() { 108 return getComponentName().flattenToShortString(); 109 } 110 getCommandActionName(@onNull String eventActionName)111 static String getCommandActionName(@NonNull String eventActionName) { 112 return eventActionName + ".command"; 113 } 114 115 private final HandlerThread mHandlerThread = new HandlerThread("CommandReceiver"); 116 117 private final Handler mMainHandler = new Handler(); 118 119 private static final class CommandReceiver extends BroadcastReceiver { 120 @NonNull 121 private final String mActionName; 122 @NonNull 123 private final Consumer<ImeCommand> mOnReceiveCommand; 124 CommandReceiver(@onNull String actionName, @NonNull Consumer<ImeCommand> onReceiveCommand)125 CommandReceiver(@NonNull String actionName, 126 @NonNull Consumer<ImeCommand> onReceiveCommand) { 127 mActionName = actionName; 128 mOnReceiveCommand = onReceiveCommand; 129 } 130 131 @Override onReceive(Context context, Intent intent)132 public void onReceive(Context context, Intent intent) { 133 if (TextUtils.equals(mActionName, intent.getAction())) { 134 mOnReceiveCommand.accept(ImeCommand.fromBundle(intent.getExtras())); 135 } 136 } 137 } 138 139 @Nullable 140 private InputConnection mMemorizedInputConnection = null; 141 142 @Nullable 143 @MainThread getMemorizedOrCurrentInputConnection()144 private InputConnection getMemorizedOrCurrentInputConnection() { 145 return mMemorizedInputConnection != null 146 ? mMemorizedInputConnection : getCurrentInputConnection(); 147 } 148 149 @WorkerThread onReceiveCommand(@onNull ImeCommand command)150 private void onReceiveCommand(@NonNull ImeCommand command) { 151 getTracer().onReceiveCommand(command, () -> { 152 if (command.shouldDispatchToMainThread()) { 153 mMainHandler.post(() -> onHandleCommand(command)); 154 } else { 155 onHandleCommand(command); 156 } 157 }); 158 } 159 160 @AnyThread onHandleCommand(@onNull ImeCommand command)161 private void onHandleCommand(@NonNull ImeCommand command) { 162 getTracer().onHandleCommand(command, () -> { 163 if (command.shouldDispatchToMainThread()) { 164 if (Looper.myLooper() != Looper.getMainLooper()) { 165 throw new IllegalStateException("command " + command 166 + " should be handled on the main thread"); 167 } 168 // The context which created from InputMethodService#createXXXContext must behave 169 // like an UI context, which can obtain a display, a window manager, 170 // a view configuration and a gesture detector instance without strict mode 171 // violation. 172 final Configuration testConfig = new Configuration(); 173 testConfig.setToDefaults(); 174 final Context configContext = createConfigurationContext(testConfig); 175 final Context attrContext = createAttributionContext(null /* attributionTag */); 176 // UI component accesses on a display context must throw strict mode violations. 177 final Context displayContext = createDisplayContext(getDisplay()); 178 switch (command.getName()) { 179 case "memorizeCurrentInputConnection": { 180 if (!Looper.getMainLooper().isCurrentThread()) { 181 return new UnsupportedOperationException( 182 "memorizeCurrentInputConnection can be requested only for the" 183 + " main thread."); 184 } 185 mMemorizedInputConnection = getCurrentInputConnection(); 186 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 187 } 188 case "unmemorizeCurrentInputConnection": { 189 if (!Looper.getMainLooper().isCurrentThread()) { 190 return new UnsupportedOperationException( 191 "unmemorizeCurrentInputConnection can be requested only for the" 192 + " main thread."); 193 } 194 mMemorizedInputConnection = null; 195 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 196 } 197 case "getTextBeforeCursor": { 198 final int n = command.getExtras().getInt("n"); 199 final int flag = command.getExtras().getInt("flag"); 200 return getMemorizedOrCurrentInputConnection().getTextBeforeCursor(n, flag); 201 } 202 case "getTextAfterCursor": { 203 final int n = command.getExtras().getInt("n"); 204 final int flag = command.getExtras().getInt("flag"); 205 return getMemorizedOrCurrentInputConnection().getTextAfterCursor(n, flag); 206 } 207 case "getSelectedText": { 208 final int flag = command.getExtras().getInt("flag"); 209 return getMemorizedOrCurrentInputConnection().getSelectedText(flag); 210 } 211 case "getCursorCapsMode": { 212 final int reqModes = command.getExtras().getInt("reqModes"); 213 return getMemorizedOrCurrentInputConnection().getCursorCapsMode(reqModes); 214 } 215 case "getExtractedText": { 216 final ExtractedTextRequest request = 217 command.getExtras().getParcelable("request"); 218 final int flags = command.getExtras().getInt("flags"); 219 return getMemorizedOrCurrentInputConnection().getExtractedText(request, 220 flags); 221 } 222 case "deleteSurroundingText": { 223 final int beforeLength = command.getExtras().getInt("beforeLength"); 224 final int afterLength = command.getExtras().getInt("afterLength"); 225 return getMemorizedOrCurrentInputConnection().deleteSurroundingText( 226 beforeLength, afterLength); 227 } 228 case "deleteSurroundingTextInCodePoints": { 229 final int beforeLength = command.getExtras().getInt("beforeLength"); 230 final int afterLength = command.getExtras().getInt("afterLength"); 231 return getMemorizedOrCurrentInputConnection() 232 .deleteSurroundingTextInCodePoints(beforeLength, afterLength); 233 } 234 case "setComposingText": { 235 final CharSequence text = command.getExtras().getCharSequence("text"); 236 final int newCursorPosition = 237 command.getExtras().getInt("newCursorPosition"); 238 return getMemorizedOrCurrentInputConnection().setComposingText( 239 text, newCursorPosition); 240 } 241 case "setComposingRegion": { 242 final int start = command.getExtras().getInt("start"); 243 final int end = command.getExtras().getInt("end"); 244 return getMemorizedOrCurrentInputConnection().setComposingRegion(start, 245 end); 246 } 247 case "finishComposingText": 248 return getMemorizedOrCurrentInputConnection().finishComposingText(); 249 case "commitText": { 250 final CharSequence text = command.getExtras().getCharSequence("text"); 251 final int newCursorPosition = 252 command.getExtras().getInt("newCursorPosition"); 253 return getMemorizedOrCurrentInputConnection().commitText(text, 254 newCursorPosition); 255 } 256 case "commitCompletion": { 257 final CompletionInfo text = command.getExtras().getParcelable("text"); 258 return getMemorizedOrCurrentInputConnection().commitCompletion(text); 259 } 260 case "commitCorrection": { 261 final CorrectionInfo correctionInfo = 262 command.getExtras().getParcelable("correctionInfo"); 263 return getMemorizedOrCurrentInputConnection().commitCorrection( 264 correctionInfo); 265 } 266 case "setSelection": { 267 final int start = command.getExtras().getInt("start"); 268 final int end = command.getExtras().getInt("end"); 269 return getMemorizedOrCurrentInputConnection().setSelection(start, end); 270 } 271 case "performEditorAction": { 272 final int editorAction = command.getExtras().getInt("editorAction"); 273 return getMemorizedOrCurrentInputConnection().performEditorAction( 274 editorAction); 275 } 276 case "performContextMenuAction": { 277 final int id = command.getExtras().getInt("id"); 278 return getMemorizedOrCurrentInputConnection().performContextMenuAction(id); 279 } 280 case "beginBatchEdit": 281 return getMemorizedOrCurrentInputConnection().beginBatchEdit(); 282 case "endBatchEdit": 283 return getMemorizedOrCurrentInputConnection().endBatchEdit(); 284 case "sendKeyEvent": { 285 final KeyEvent event = command.getExtras().getParcelable("event"); 286 return getMemorizedOrCurrentInputConnection().sendKeyEvent(event); 287 } 288 case "clearMetaKeyStates": { 289 final int states = command.getExtras().getInt("states"); 290 return getMemorizedOrCurrentInputConnection().clearMetaKeyStates(states); 291 } 292 case "reportFullscreenMode": { 293 final boolean enabled = command.getExtras().getBoolean("enabled"); 294 return getMemorizedOrCurrentInputConnection().reportFullscreenMode(enabled); 295 } 296 case "performSpellCheck": { 297 return getMemorizedOrCurrentInputConnection().performSpellCheck(); 298 } 299 case "performPrivateCommand": { 300 final String action = command.getExtras().getString("action"); 301 final Bundle data = command.getExtras().getBundle("data"); 302 return getMemorizedOrCurrentInputConnection().performPrivateCommand(action, 303 data); 304 } 305 case "requestCursorUpdates": { 306 final int cursorUpdateMode = command.getExtras().getInt("cursorUpdateMode"); 307 return getMemorizedOrCurrentInputConnection().requestCursorUpdates( 308 cursorUpdateMode); 309 } 310 case "getHandler": 311 return getMemorizedOrCurrentInputConnection().getHandler(); 312 case "closeConnection": 313 getMemorizedOrCurrentInputConnection().closeConnection(); 314 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 315 case "commitContent": { 316 final InputContentInfo inputContentInfo = 317 command.getExtras().getParcelable("inputContentInfo"); 318 final int flags = command.getExtras().getInt("flags"); 319 final Bundle opts = command.getExtras().getBundle("opts"); 320 return getMemorizedOrCurrentInputConnection().commitContent( 321 inputContentInfo, flags, opts); 322 } 323 case "setBackDisposition": { 324 final int backDisposition = 325 command.getExtras().getInt("backDisposition"); 326 setBackDisposition(backDisposition); 327 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 328 } 329 case "requestHideSelf": { 330 final int flags = command.getExtras().getInt("flags"); 331 requestHideSelf(flags); 332 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 333 } 334 case "requestShowSelf": { 335 final int flags = command.getExtras().getInt("flags"); 336 requestShowSelf(flags); 337 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 338 } 339 case "sendDownUpKeyEvents": { 340 final int keyEventCode = command.getExtras().getInt("keyEventCode"); 341 sendDownUpKeyEvents(keyEventCode); 342 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 343 } 344 case "getApplicationInfo": { 345 final String packageName = command.getExtras().getString("packageName"); 346 final int flags = command.getExtras().getInt("flags"); 347 try { 348 return getPackageManager().getApplicationInfo(packageName, flags); 349 } catch (PackageManager.NameNotFoundException e) { 350 return e; 351 } 352 } 353 case "getDisplayId": 354 return getDisplay().getDisplayId(); 355 case "verifyLayoutInflaterContext": 356 return getLayoutInflater().getContext() == this; 357 case "setHeight": 358 final int height = command.getExtras().getInt("height"); 359 mView.setHeight(height); 360 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 361 case "setInlineSuggestionsExtras": 362 mInlineSuggestionsExtras = command.getExtras(); 363 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 364 case "verifyGetDisplay": 365 try { 366 return verifyGetDisplay(); 367 } catch (UnsupportedOperationException e) { 368 return e; 369 } 370 case "verifyGetWindowManager": { 371 final WindowManager imsWm = getSystemService(WindowManager.class); 372 final WindowManager configContextWm = 373 configContext.getSystemService(WindowManager.class); 374 final WindowManager attrContextWm = 375 attrContext.getSystemService(WindowManager.class); 376 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 377 } 378 case "verifyGetViewConfiguration": { 379 final ViewConfiguration imsViewConfig = ViewConfiguration.get(this); 380 final ViewConfiguration configContextViewConfig = 381 ViewConfiguration.get(configContext); 382 final ViewConfiguration attrContextViewConfig = 383 ViewConfiguration.get(attrContext); 384 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 385 } 386 case "verifyGetGestureDetector": { 387 GestureDetector.SimpleOnGestureListener listener = 388 new GestureDetector.SimpleOnGestureListener(); 389 final GestureDetector imsGestureDetector = 390 new GestureDetector(this, listener); 391 final GestureDetector configContextGestureDetector = 392 new GestureDetector(configContext, listener); 393 final GestureDetector attrGestureDetector = 394 new GestureDetector(attrContext, listener); 395 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 396 } 397 case "verifyGetWindowManagerOnDisplayContext": { 398 // Obtaining a WindowManager on a display context must throw a strict mode 399 // violation. 400 final WindowManager wm = displayContext 401 .getSystemService(WindowManager.class); 402 403 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 404 } 405 case "verifyGetViewConfigurationOnDisplayContext": { 406 // Obtaining a ViewConfiguration on a display context must throw a strict 407 // mode violation. 408 final ViewConfiguration viewConfiguration = 409 ViewConfiguration.get(displayContext); 410 411 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 412 } 413 case "verifyGetGestureDetectorOnDisplayContext": { 414 // Obtaining a GestureDetector on a display context must throw a strict mode 415 // violation. 416 GestureDetector.SimpleOnGestureListener listener = 417 new GestureDetector.SimpleOnGestureListener(); 418 final GestureDetector gestureDetector = 419 new GestureDetector(displayContext, listener); 420 421 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 422 } 423 } 424 } 425 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 426 }); 427 } 428 verifyGetDisplay()429 private boolean verifyGetDisplay() throws UnsupportedOperationException { 430 final Display display; 431 final Display configContextDisplay; 432 final Configuration config = new Configuration(); 433 config.setToDefaults(); 434 final Context configContext = createConfigurationContext(config); 435 display = getDisplay(); 436 configContextDisplay = configContext.getDisplay(); 437 return display != null && configContextDisplay != null; 438 } 439 440 @Nullable 441 private Bundle mInlineSuggestionsExtras; 442 443 @Nullable 444 private CommandReceiver mCommandReceiver; 445 446 @Nullable 447 private ImeSettings mSettings; 448 449 private final AtomicReference<String> mImeEventActionName = new AtomicReference<>(); 450 451 @Nullable getImeEventActionName()452 String getImeEventActionName() { 453 return mImeEventActionName.get(); 454 } 455 456 private final AtomicReference<String> mClientPackageName = new AtomicReference<>(); 457 458 @Nullable getClientPackageName()459 String getClientPackageName() { 460 return mClientPackageName.get(); 461 } 462 463 private class MockInputMethodImpl extends InputMethodImpl { 464 @Override showSoftInput(int flags, ResultReceiver resultReceiver)465 public void showSoftInput(int flags, ResultReceiver resultReceiver) { 466 getTracer().showSoftInput(flags, resultReceiver, 467 () -> super.showSoftInput(flags, resultReceiver)); 468 } 469 470 @Override hideSoftInput(int flags, ResultReceiver resultReceiver)471 public void hideSoftInput(int flags, ResultReceiver resultReceiver) { 472 getTracer().hideSoftInput(flags, resultReceiver, 473 () -> super.hideSoftInput(flags, resultReceiver)); 474 } 475 476 @Override attachToken(IBinder token)477 public void attachToken(IBinder token) { 478 getTracer().attachToken(token, () -> super.attachToken(token)); 479 } 480 481 @Override bindInput(InputBinding binding)482 public void bindInput(InputBinding binding) { 483 getTracer().bindInput(binding, () -> super.bindInput(binding)); 484 } 485 486 @Override unbindInput()487 public void unbindInput() { 488 getTracer().unbindInput(() -> super.unbindInput()); 489 } 490 } 491 492 @Override onCreate()493 public void onCreate() { 494 // Initialize minimum settings to send events in Tracer#onCreate(). 495 mSettings = SettingsProvider.getSettings(); 496 if (mSettings == null) { 497 throw new IllegalStateException("Settings file is not found. " 498 + "Make sure MockImeSession.create() is used to launch Mock IME."); 499 } 500 mClientPackageName.set(mSettings.getClientPackageName()); 501 mImeEventActionName.set(mSettings.getEventCallbackActionName()); 502 503 // TODO(b/159593676): consider to detect more violations 504 if (mSettings.isStrictModeEnabled()) { 505 StrictMode.setVmPolicy( 506 new StrictMode.VmPolicy.Builder() 507 .detectIncorrectContextUse() 508 .penaltyLog() 509 .penaltyListener(Runnable::run, 510 v -> getTracer().onStrictModeViolated(() -> { })) 511 .build()); 512 } 513 514 getTracer().onCreate(() -> { 515 super.onCreate(); 516 mHandlerThread.start(); 517 final String actionName = getCommandActionName(mSettings.getEventCallbackActionName()); 518 mCommandReceiver = new CommandReceiver(actionName, this::onReceiveCommand); 519 final IntentFilter filter = new IntentFilter(actionName); 520 final Handler handler = new Handler(mHandlerThread.getLooper()); 521 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 522 registerReceiver(mCommandReceiver, filter, null /* broadcastPermission */, handler, 523 Context.RECEIVER_VISIBLE_TO_INSTANT_APPS); 524 } else { 525 registerReceiver(mCommandReceiver, filter, null /* broadcastPermission */, handler); 526 } 527 if (mSettings.isVerifyGetDisplayOnCreate()) { 528 getTracer().onVerify("getDisplay", this::verifyGetDisplay); 529 } 530 final int windowFlags = mSettings.getWindowFlags(0); 531 final int windowFlagsMask = mSettings.getWindowFlagsMask(0); 532 if (windowFlags != 0 || windowFlagsMask != 0) { 533 final int prevFlags = getWindow().getWindow().getAttributes().flags; 534 getWindow().getWindow().setFlags(windowFlags, windowFlagsMask); 535 // For some reasons, seems that we need to post another requestLayout() when 536 // FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS bit is changed. 537 // TODO: Investigate the reason. 538 if ((windowFlagsMask & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0) { 539 final boolean hadFlag = (prevFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0; 540 final boolean hasFlag = (windowFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0; 541 if (hadFlag != hasFlag) { 542 final View decorView = getWindow().getWindow().getDecorView(); 543 decorView.post(() -> decorView.requestLayout()); 544 } 545 } 546 } 547 548 // Ensuring bar contrast interferes with the tests. 549 getWindow().getWindow().setStatusBarContrastEnforced(false); 550 getWindow().getWindow().setNavigationBarContrastEnforced(false); 551 552 if (mSettings.hasNavigationBarColor()) { 553 getWindow().getWindow().setNavigationBarColor(mSettings.getNavigationBarColor()); 554 } 555 }); 556 } 557 558 @Override onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly)559 public void onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly) { 560 getTracer().onConfigureWindow(win, isFullscreen, isCandidatesOnly, 561 () -> super.onConfigureWindow(win, isFullscreen, isCandidatesOnly)); 562 } 563 564 @Override onEvaluateFullscreenMode()565 public boolean onEvaluateFullscreenMode() { 566 return getTracer().onEvaluateFullscreenMode(() -> 567 mSettings.fullscreenModeAllowed(false) && super.onEvaluateFullscreenMode()); 568 } 569 570 private static final class KeyboardLayoutView extends LinearLayout { 571 @NonNull 572 private final MockIme mMockIme; 573 @NonNull 574 private final ImeSettings mSettings; 575 @NonNull 576 private final View.OnLayoutChangeListener mLayoutListener; 577 578 private final LinearLayout mLayout; 579 580 @Nullable 581 private final LinearLayout mSuggestionView; 582 583 private boolean mDrawsBehindNavBar = false; 584 KeyboardLayoutView(MockIme mockIme, @NonNull ImeSettings imeSettings, @Nullable Consumer<ImeLayoutInfo> onInputViewLayoutChangedCallback)585 KeyboardLayoutView(MockIme mockIme, @NonNull ImeSettings imeSettings, 586 @Nullable Consumer<ImeLayoutInfo> onInputViewLayoutChangedCallback) { 587 super(mockIme); 588 589 mMockIme = mockIme; 590 mSettings = imeSettings; 591 592 setOrientation(VERTICAL); 593 594 final int defaultBackgroundColor = 595 getResources().getColor(android.R.color.holo_orange_dark, null); 596 597 final int mainSpacerHeight = mSettings.getInputViewHeight(LayoutParams.WRAP_CONTENT); 598 mLayout = new LinearLayout(getContext()); 599 mLayout.setOrientation(LinearLayout.VERTICAL); 600 601 if (mSettings.getInlineSuggestionsEnabled()) { 602 final HorizontalScrollView scrollView = new HorizontalScrollView(getContext()); 603 final LayoutParams scrollViewParams = new LayoutParams(MATCH_PARENT, 100); 604 scrollView.setLayoutParams(scrollViewParams); 605 606 final LinearLayout suggestionView = new LinearLayout(getContext()); 607 suggestionView.setBackgroundColor(0xFFEEEEEE); 608 final String suggestionViewContentDesc = 609 mSettings.getInlineSuggestionViewContentDesc(null /* default */); 610 if (suggestionViewContentDesc != null) { 611 suggestionView.setContentDescription(suggestionViewContentDesc); 612 } 613 scrollView.addView(suggestionView, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); 614 mSuggestionView = suggestionView; 615 616 mLayout.addView(scrollView); 617 } else { 618 mSuggestionView = null; 619 } 620 621 { 622 final FrameLayout secondaryLayout = new FrameLayout(getContext()); 623 secondaryLayout.setForegroundGravity(Gravity.CENTER); 624 625 final TextView textView = new TextView(getContext()); 626 textView.setLayoutParams(new LayoutParams(MATCH_PARENT, WRAP_CONTENT)); 627 textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); 628 textView.setGravity(Gravity.CENTER); 629 textView.setText(getImeId()); 630 textView.setBackgroundColor( 631 mSettings.getBackgroundColor(defaultBackgroundColor)); 632 secondaryLayout.addView(textView); 633 634 if (mSettings.isWatermarkEnabled(true /* defaultValue */)) { 635 final ImageView imageView = new ImageView(getContext()); 636 final Bitmap bitmap = Watermark.create(); 637 imageView.setImageBitmap(bitmap); 638 secondaryLayout.addView(imageView, 639 new FrameLayout.LayoutParams(bitmap.getWidth(), bitmap.getHeight(), 640 Gravity.CENTER)); 641 } 642 643 mLayout.addView(secondaryLayout); 644 } 645 646 addView(mLayout, MATCH_PARENT, mainSpacerHeight); 647 648 final int systemUiVisibility = mSettings.getInputViewSystemUiVisibility(0); 649 if (systemUiVisibility != 0) { 650 setSystemUiVisibility(systemUiVisibility); 651 } 652 653 if (mSettings.getDrawsBehindNavBar()) { 654 mDrawsBehindNavBar = true; 655 mMockIme.getWindow().getWindow().setDecorFitsSystemWindows(false); 656 setSystemUiVisibility(getSystemUiVisibility() 657 | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); 658 } 659 660 mLayoutListener = (View v, int left, int top, int right, int bottom, int oldLeft, 661 int oldTop, int oldRight, int oldBottom) -> 662 onInputViewLayoutChangedCallback.accept( 663 ImeLayoutInfo.fromLayoutListenerCallback( 664 v, left, top, right, bottom, oldLeft, oldTop, oldRight, 665 oldBottom)); 666 this.addOnLayoutChangeListener(mLayoutListener); 667 } 668 setHeight(int height)669 private void setHeight(int height) { 670 mLayout.getLayoutParams().height = height; 671 mLayout.requestLayout(); 672 } 673 updateBottomPaddingIfNecessary(int newPaddingBottom)674 private void updateBottomPaddingIfNecessary(int newPaddingBottom) { 675 if (getPaddingBottom() != newPaddingBottom) { 676 setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), newPaddingBottom); 677 } 678 } 679 680 @Override onApplyWindowInsets(WindowInsets insets)681 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 682 if (insets.isConsumed() 683 || mDrawsBehindNavBar 684 || (getSystemUiVisibility() & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0) { 685 // In this case we are not interested in consuming NavBar region. 686 // Make sure that the bottom padding is empty. 687 updateBottomPaddingIfNecessary(0); 688 return insets; 689 } 690 691 // In some cases the bottom system window inset is not a navigation bar. Wear devices 692 // that have bottom chin are examples. For now, assume that it's a navigation bar if it 693 // has the same height as the root window's stable bottom inset. 694 final WindowInsets rootWindowInsets = getRootWindowInsets(); 695 if (rootWindowInsets != null && (rootWindowInsets.getStableInsetBottom() 696 != insets.getSystemWindowInsetBottom())) { 697 // This is probably not a NavBar. 698 updateBottomPaddingIfNecessary(0); 699 return insets; 700 } 701 702 final int possibleNavBarHeight = insets.getSystemWindowInsetBottom(); 703 updateBottomPaddingIfNecessary(possibleNavBarHeight); 704 return possibleNavBarHeight <= 0 705 ? insets 706 : insets.replaceSystemWindowInsets( 707 insets.getSystemWindowInsetLeft(), 708 insets.getSystemWindowInsetTop(), 709 insets.getSystemWindowInsetRight(), 710 0 /* bottom */); 711 } 712 713 @Override onWindowVisibilityChanged(int visibility)714 protected void onWindowVisibilityChanged(int visibility) { 715 mMockIme.getTracer().onWindowVisibilityChanged(() -> { 716 super.onWindowVisibilityChanged(visibility); 717 }, visibility); 718 } 719 720 @Override onDetachedFromWindow()721 protected void onDetachedFromWindow() { 722 super.onDetachedFromWindow(); 723 removeOnLayoutChangeListener(mLayoutListener); 724 } 725 726 @MainThread updateInlineSuggestions( @onNull PendingInlineSuggestions pendingInlineSuggestions)727 private void updateInlineSuggestions( 728 @NonNull PendingInlineSuggestions pendingInlineSuggestions) { 729 Log.d(TAG, "updateInlineSuggestions() called: " + pendingInlineSuggestions.mTotalCount); 730 if (mSuggestionView == null || !pendingInlineSuggestions.mValid.get()) { 731 return; 732 } 733 mSuggestionView.removeAllViews(); 734 for (int i = 0; i < pendingInlineSuggestions.mTotalCount; i++) { 735 View view = pendingInlineSuggestions.mViews[i]; 736 if (view == null) { 737 continue; 738 } 739 mSuggestionView.addView(view); 740 } 741 } 742 } 743 744 KeyboardLayoutView mView; 745 onInputViewLayoutChanged(@onNull ImeLayoutInfo layoutInfo)746 private void onInputViewLayoutChanged(@NonNull ImeLayoutInfo layoutInfo) { 747 getTracer().onInputViewLayoutChanged(layoutInfo, () -> { }); 748 } 749 750 @Override onCreateInputView()751 public View onCreateInputView() { 752 return getTracer().onCreateInputView(() -> { 753 mView = new KeyboardLayoutView(this, mSettings, this::onInputViewLayoutChanged); 754 return mView; 755 }); 756 } 757 758 @Override onStartInput(EditorInfo editorInfo, boolean restarting)759 public void onStartInput(EditorInfo editorInfo, boolean restarting) { 760 getTracer().onStartInput(editorInfo, restarting, 761 () -> super.onStartInput(editorInfo, restarting)); 762 } 763 764 @Override onStartInputView(EditorInfo editorInfo, boolean restarting)765 public void onStartInputView(EditorInfo editorInfo, boolean restarting) { 766 getTracer().onStartInputView(editorInfo, restarting, 767 () -> super.onStartInputView(editorInfo, restarting)); 768 } 769 770 @Override onFinishInputView(boolean finishingInput)771 public void onFinishInputView(boolean finishingInput) { 772 getTracer().onFinishInputView(finishingInput, 773 () -> super.onFinishInputView(finishingInput)); 774 } 775 776 @Override onFinishInput()777 public void onFinishInput() { 778 getTracer().onFinishInput(() -> super.onFinishInput()); 779 } 780 781 @Override onKeyDown(int keyCode, KeyEvent event)782 public boolean onKeyDown(int keyCode, KeyEvent event) { 783 return getTracer().onKeyDown(keyCode, event, () -> super.onKeyDown(keyCode, event)); 784 } 785 786 @Override onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo)787 public void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) { 788 getTracer().onUpdateCursorAnchorInfo(cursorAnchorInfo, 789 () -> super.onUpdateCursorAnchorInfo(cursorAnchorInfo)); 790 } 791 792 @Override onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd)793 public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, 794 int candidatesStart, int candidatesEnd) { 795 getTracer().onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 796 candidatesStart, candidatesEnd, 797 () -> super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 798 candidatesStart, candidatesEnd)); 799 } 800 801 @CallSuper onEvaluateInputViewShown()802 public boolean onEvaluateInputViewShown() { 803 return getTracer().onEvaluateInputViewShown(() -> { 804 // onShowInputRequested() is indeed @CallSuper so we always call this, even when the 805 // result is ignored. 806 final boolean originalResult = super.onEvaluateInputViewShown(); 807 if (!mSettings.getHardKeyboardConfigurationBehaviorAllowed(false)) { 808 final Configuration config = getResources().getConfiguration(); 809 if (config.keyboard != Configuration.KEYBOARD_NOKEYS 810 && config.hardKeyboardHidden != Configuration.HARDKEYBOARDHIDDEN_YES) { 811 // Override the behavior of InputMethodService#onEvaluateInputViewShown() 812 return true; 813 } 814 } 815 return originalResult; 816 }); 817 } 818 819 @Override 820 public boolean onShowInputRequested(int flags, boolean configChange) { 821 return getTracer().onShowInputRequested(flags, configChange, () -> { 822 // onShowInputRequested() is not marked with @CallSuper, but just in case. 823 final boolean originalResult = super.onShowInputRequested(flags, configChange); 824 if (!mSettings.getHardKeyboardConfigurationBehaviorAllowed(false)) { 825 if ((flags & InputMethod.SHOW_EXPLICIT) == 0 826 && getResources().getConfiguration().keyboard 827 != Configuration.KEYBOARD_NOKEYS) { 828 // Override the behavior of InputMethodService#onShowInputRequested() 829 return true; 830 } 831 } 832 return originalResult; 833 }); 834 } 835 836 @Override 837 public void onDestroy() { 838 getTracer().onDestroy(() -> { 839 super.onDestroy(); 840 unregisterReceiver(mCommandReceiver); 841 mHandlerThread.quitSafely(); 842 }); 843 } 844 845 @Override 846 public AbstractInputMethodImpl onCreateInputMethodInterface() { 847 return getTracer().onCreateInputMethodInterface(() -> new MockInputMethodImpl()); 848 } 849 850 private final ThreadLocal<Tracer> mThreadLocalTracer = new ThreadLocal<>(); 851 852 private Tracer getTracer() { 853 Tracer tracer = mThreadLocalTracer.get(); 854 if (tracer == null) { 855 tracer = new Tracer(this); 856 mThreadLocalTracer.set(tracer); 857 } 858 return tracer; 859 } 860 861 @NonNull 862 private ImeState getState() { 863 final boolean hasInputBinding = getCurrentInputBinding() != null; 864 final boolean hasFallbackInputConnection = 865 !hasInputBinding 866 || getCurrentInputConnection() == getCurrentInputBinding().getConnection(); 867 return new ImeState(hasInputBinding, hasFallbackInputConnection); 868 } 869 870 private PendingInlineSuggestions mPendingInlineSuggestions; 871 872 private static final class PendingInlineSuggestions { 873 final InlineSuggestionsResponse mResponse; 874 final int mTotalCount; 875 final View[] mViews; 876 final AtomicInteger mInflatedViewCount; 877 final AtomicBoolean mValid = new AtomicBoolean(true); 878 879 PendingInlineSuggestions(InlineSuggestionsResponse response) { 880 mResponse = response; 881 mTotalCount = response.getInlineSuggestions().size(); 882 mViews = new View[mTotalCount]; 883 mInflatedViewCount = new AtomicInteger(0); 884 } 885 } 886 887 @MainThread 888 @Override 889 public InlineSuggestionsRequest onCreateInlineSuggestionsRequest(Bundle uiExtras) { 890 StylesBuilder stylesBuilder = UiVersions.newStylesBuilder(); 891 stylesBuilder.addStyle(InlineSuggestionUi.newStyleBuilder().build()); 892 Bundle styles = stylesBuilder.build(); 893 894 if (mInlineSuggestionsExtras != null) { 895 styles.putAll(mInlineSuggestionsExtras); 896 } 897 898 return getTracer().onCreateInlineSuggestionsRequest(() -> { 899 final ArrayList<InlinePresentationSpec> presentationSpecs = new ArrayList<>(); 900 presentationSpecs.add(new InlinePresentationSpec.Builder(new Size(100, 100), 901 new Size(400, 100)).setStyle(styles).build()); 902 presentationSpecs.add(new InlinePresentationSpec.Builder(new Size(100, 100), 903 new Size(400, 100)).setStyle(styles).build()); 904 905 final InlinePresentationSpec tooltipSpec = 906 new InlinePresentationSpec.Builder(new Size(100, 100), 907 new Size(400, 100)).setStyle(styles).build(); 908 final InlineSuggestionsRequest.Builder builder = 909 new InlineSuggestionsRequest.Builder(presentationSpecs) 910 .setInlineTooltipPresentationSpec(tooltipSpec) 911 .setMaxSuggestionCount(6); 912 if (mInlineSuggestionsExtras != null) { 913 builder.setExtras(mInlineSuggestionsExtras.deepCopy()); 914 } 915 return builder.build(); 916 }); 917 } 918 919 @MainThread 920 @Override 921 public boolean onInlineSuggestionsResponse(@NonNull InlineSuggestionsResponse response) { 922 return getTracer().onInlineSuggestionsResponse(response, () -> { 923 final PendingInlineSuggestions pendingInlineSuggestions = 924 new PendingInlineSuggestions(response); 925 if (mPendingInlineSuggestions != null) { 926 mPendingInlineSuggestions.mValid.set(false); 927 } 928 mPendingInlineSuggestions = pendingInlineSuggestions; 929 if (pendingInlineSuggestions.mTotalCount == 0) { 930 if (mView != null) { 931 mView.updateInlineSuggestions(pendingInlineSuggestions); 932 } 933 return true; 934 } 935 936 final ExecutorService executorService = Executors.newCachedThreadPool(); 937 for (int i = 0; i < pendingInlineSuggestions.mTotalCount; i++) { 938 final int index = i; 939 InlineSuggestion inlineSuggestion = 940 pendingInlineSuggestions.mResponse.getInlineSuggestions().get(index); 941 inlineSuggestion.inflate( 942 this, 943 new Size(WRAP_CONTENT, WRAP_CONTENT), 944 executorService, 945 suggestionView -> { 946 Log.d(TAG, "new inline suggestion view ready"); 947 if (suggestionView != null) { 948 suggestionView.setOnClickListener((v) -> { 949 getTracer().onInlineSuggestionClickedEvent(() -> { }); 950 }); 951 suggestionView.setOnLongClickListener((v) -> { 952 getTracer().onInlineSuggestionLongClickedEvent(() -> { }); 953 return true; 954 }); 955 pendingInlineSuggestions.mViews[index] = suggestionView; 956 } 957 if (pendingInlineSuggestions.mInflatedViewCount.incrementAndGet() 958 == pendingInlineSuggestions.mTotalCount 959 && pendingInlineSuggestions.mValid.get()) { 960 Log.d(TAG, "ready to display all suggestions"); 961 mMainHandler.post(() -> 962 mView.updateInlineSuggestions(pendingInlineSuggestions)); 963 } 964 }); 965 } 966 return true; 967 }); 968 } 969 970 /** 971 * Event tracing helper class for {@link MockIme}. 972 */ 973 private static final class Tracer { 974 975 @NonNull 976 private final MockIme mIme; 977 978 private final int mThreadId = Process.myTid(); 979 980 @NonNull 981 private final String mThreadName = 982 Thread.currentThread().getName() != null ? Thread.currentThread().getName() : ""; 983 984 private final boolean mIsMainThread = 985 Looper.getMainLooper().getThread() == Thread.currentThread(); 986 987 private int mNestLevel = 0; 988 989 private String mImeEventActionName; 990 991 private String mClientPackageName; 992 993 Tracer(@NonNull MockIme mockIme) { 994 mIme = mockIme; 995 } 996 997 private void sendEventInternal(@NonNull ImeEvent event) { 998 if (mImeEventActionName == null) { 999 mImeEventActionName = mIme.getImeEventActionName(); 1000 } 1001 if (mClientPackageName == null) { 1002 mClientPackageName = mIme.getClientPackageName(); 1003 } 1004 if (mImeEventActionName == null || mClientPackageName == null) { 1005 Log.e(TAG, "Tracer cannot be used before onCreate()"); 1006 return; 1007 } 1008 final Intent intent = new Intent() 1009 .setAction(mImeEventActionName) 1010 .setPackage(mClientPackageName) 1011 .putExtras(event.toBundle()) 1012 .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY 1013 | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS); 1014 mIme.sendBroadcast(intent); 1015 } 1016 1017 private void recordEventInternal(@NonNull String eventName, @NonNull Runnable runnable) { 1018 recordEventInternal(eventName, runnable, new Bundle()); 1019 } 1020 1021 private void recordEventInternal(@NonNull String eventName, @NonNull Runnable runnable, 1022 @NonNull Bundle arguments) { 1023 recordEventInternal(eventName, () -> { 1024 runnable.run(); return ImeEvent.RETURN_VALUE_UNAVAILABLE; 1025 }, arguments); 1026 } 1027 1028 private <T> T recordEventInternal(@NonNull String eventName, 1029 @NonNull Supplier<T> supplier) { 1030 return recordEventInternal(eventName, supplier, new Bundle()); 1031 } 1032 1033 private <T> T recordEventInternal(@NonNull String eventName, 1034 @NonNull Supplier<T> supplier, @NonNull Bundle arguments) { 1035 final ImeState enterState = mIme.getState(); 1036 final long enterTimestamp = SystemClock.elapsedRealtimeNanos(); 1037 final long enterWallTime = System.currentTimeMillis(); 1038 final int nestLevel = mNestLevel; 1039 // Send enter event 1040 sendEventInternal(new ImeEvent(eventName, nestLevel, mThreadName, 1041 mThreadId, mIsMainThread, enterTimestamp, 0, enterWallTime, 1042 0, enterState, null, arguments, 1043 ImeEvent.RETURN_VALUE_UNAVAILABLE)); 1044 ++mNestLevel; 1045 T result; 1046 try { 1047 result = supplier.get(); 1048 } finally { 1049 --mNestLevel; 1050 } 1051 final long exitTimestamp = SystemClock.elapsedRealtimeNanos(); 1052 final long exitWallTime = System.currentTimeMillis(); 1053 final ImeState exitState = mIme.getState(); 1054 // Send exit event 1055 sendEventInternal(new ImeEvent(eventName, nestLevel, mThreadName, 1056 mThreadId, mIsMainThread, enterTimestamp, exitTimestamp, enterWallTime, 1057 exitWallTime, enterState, exitState, arguments, result)); 1058 return result; 1059 } 1060 1061 void onCreate(@NonNull Runnable runnable) { 1062 recordEventInternal("onCreate", runnable); 1063 } 1064 1065 void onVerify(String name, @NonNull BooleanSupplier supplier) { 1066 final Bundle arguments = new Bundle(); 1067 arguments.putString("name", name); 1068 recordEventInternal("onVerify", supplier::getAsBoolean, arguments); 1069 } 1070 1071 void onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly, 1072 @NonNull Runnable runnable) { 1073 final Bundle arguments = new Bundle(); 1074 arguments.putBoolean("isFullscreen", isFullscreen); 1075 arguments.putBoolean("isCandidatesOnly", isCandidatesOnly); 1076 recordEventInternal("onConfigureWindow", runnable, arguments); 1077 } 1078 1079 boolean onEvaluateFullscreenMode(@NonNull BooleanSupplier supplier) { 1080 return recordEventInternal("onEvaluateFullscreenMode", supplier::getAsBoolean); 1081 } 1082 1083 boolean onEvaluateInputViewShown(@NonNull BooleanSupplier supplier) { 1084 return recordEventInternal("onEvaluateInputViewShown", supplier::getAsBoolean); 1085 } 1086 1087 View onCreateInputView(@NonNull Supplier<View> supplier) { 1088 return recordEventInternal("onCreateInputView", supplier); 1089 } 1090 1091 void onStartInput(EditorInfo editorInfo, boolean restarting, @NonNull Runnable runnable) { 1092 final Bundle arguments = new Bundle(); 1093 arguments.putParcelable("editorInfo", editorInfo); 1094 arguments.putBoolean("restarting", restarting); 1095 recordEventInternal("onStartInput", runnable, arguments); 1096 } 1097 1098 void onWindowVisibilityChanged(@NonNull Runnable runnable, int visibility) { 1099 final Bundle arguments = new Bundle(); 1100 arguments.putInt("visible", visibility); 1101 recordEventInternal("onWindowVisibilityChanged", runnable, arguments); 1102 } 1103 1104 void onStartInputView(EditorInfo editorInfo, boolean restarting, 1105 @NonNull Runnable runnable) { 1106 final Bundle arguments = new Bundle(); 1107 arguments.putParcelable("editorInfo", editorInfo); 1108 arguments.putBoolean("restarting", restarting); 1109 recordEventInternal("onStartInputView", runnable, arguments); 1110 } 1111 1112 void onFinishInputView(boolean finishingInput, @NonNull Runnable runnable) { 1113 final Bundle arguments = new Bundle(); 1114 arguments.putBoolean("finishingInput", finishingInput); 1115 recordEventInternal("onFinishInputView", runnable, arguments); 1116 } 1117 1118 void onFinishInput(@NonNull Runnable runnable) { 1119 recordEventInternal("onFinishInput", runnable); 1120 } 1121 1122 boolean onKeyDown(int keyCode, KeyEvent event, @NonNull BooleanSupplier supplier) { 1123 final Bundle arguments = new Bundle(); 1124 arguments.putInt("keyCode", keyCode); 1125 arguments.putParcelable("event", event); 1126 return recordEventInternal("onKeyDown", supplier::getAsBoolean, arguments); 1127 } 1128 1129 void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo, 1130 @NonNull Runnable runnable) { 1131 final Bundle arguments = new Bundle(); 1132 arguments.putParcelable("cursorAnchorInfo", cursorAnchorInfo); 1133 recordEventInternal("onUpdateCursorAnchorInfo", runnable, arguments); 1134 } 1135 1136 void onUpdateSelection(int oldSelStart, 1137 int oldSelEnd, 1138 int newSelStart, 1139 int newSelEnd, 1140 int candidatesStart, 1141 int candidatesEnd, 1142 @NonNull Runnable runnable) { 1143 final Bundle arguments = new Bundle(); 1144 arguments.putInt("oldSelStart", oldSelStart); 1145 arguments.putInt("oldSelEnd", oldSelEnd); 1146 arguments.putInt("newSelStart", newSelStart); 1147 arguments.putInt("newSelEnd", newSelEnd); 1148 arguments.putInt("candidatesStart", candidatesStart); 1149 arguments.putInt("candidatesEnd", candidatesEnd); 1150 recordEventInternal("onUpdateSelection", runnable, arguments); 1151 } 1152 1153 boolean onShowInputRequested(int flags, boolean configChange, 1154 @NonNull BooleanSupplier supplier) { 1155 final Bundle arguments = new Bundle(); 1156 arguments.putInt("flags", flags); 1157 arguments.putBoolean("configChange", configChange); 1158 return recordEventInternal("onShowInputRequested", supplier::getAsBoolean, arguments); 1159 } 1160 1161 void onDestroy(@NonNull Runnable runnable) { 1162 recordEventInternal("onDestroy", runnable); 1163 } 1164 1165 void attachToken(IBinder token, @NonNull Runnable runnable) { 1166 final Bundle arguments = new Bundle(); 1167 arguments.putBinder("token", token); 1168 recordEventInternal("attachToken", runnable, arguments); 1169 } 1170 1171 void bindInput(InputBinding binding, @NonNull Runnable runnable) { 1172 final Bundle arguments = new Bundle(); 1173 arguments.putParcelable("binding", binding); 1174 recordEventInternal("bindInput", runnable, arguments); 1175 } 1176 1177 void unbindInput(@NonNull Runnable runnable) { 1178 recordEventInternal("unbindInput", runnable); 1179 } 1180 1181 void showSoftInput(int flags, ResultReceiver resultReceiver, @NonNull Runnable runnable) { 1182 final Bundle arguments = new Bundle(); 1183 arguments.putInt("flags", flags); 1184 arguments.putParcelable("resultReceiver", resultReceiver); 1185 recordEventInternal("showSoftInput", runnable, arguments); 1186 } 1187 1188 void hideSoftInput(int flags, ResultReceiver resultReceiver, @NonNull Runnable runnable) { 1189 final Bundle arguments = new Bundle(); 1190 arguments.putInt("flags", flags); 1191 arguments.putParcelable("resultReceiver", resultReceiver); 1192 recordEventInternal("hideSoftInput", runnable, arguments); 1193 } 1194 1195 AbstractInputMethodImpl onCreateInputMethodInterface( 1196 @NonNull Supplier<AbstractInputMethodImpl> supplier) { 1197 return recordEventInternal("onCreateInputMethodInterface", supplier); 1198 } 1199 1200 void onReceiveCommand(@NonNull ImeCommand command, @NonNull Runnable runnable) { 1201 final Bundle arguments = new Bundle(); 1202 arguments.putBundle("command", command.toBundle()); 1203 recordEventInternal("onReceiveCommand", runnable, arguments); 1204 } 1205 1206 void onHandleCommand( 1207 @NonNull ImeCommand command, @NonNull Supplier<Object> resultSupplier) { 1208 final Bundle arguments = new Bundle(); 1209 arguments.putBundle("command", command.toBundle()); 1210 recordEventInternal("onHandleCommand", resultSupplier, arguments); 1211 } 1212 1213 void onInputViewLayoutChanged(@NonNull ImeLayoutInfo imeLayoutInfo, 1214 @NonNull Runnable runnable) { 1215 final Bundle arguments = new Bundle(); 1216 imeLayoutInfo.writeToBundle(arguments); 1217 recordEventInternal("onInputViewLayoutChanged", runnable, arguments); 1218 } 1219 1220 void onStrictModeViolated(@NonNull Runnable runnable) { 1221 final Bundle arguments = new Bundle(); 1222 recordEventInternal("onStrictModeViolated", runnable, arguments); 1223 } 1224 1225 InlineSuggestionsRequest onCreateInlineSuggestionsRequest( 1226 @NonNull Supplier<InlineSuggestionsRequest> supplier) { 1227 return recordEventInternal("onCreateInlineSuggestionsRequest", supplier); 1228 } 1229 1230 boolean onInlineSuggestionsResponse(@NonNull InlineSuggestionsResponse response, 1231 @NonNull BooleanSupplier supplier) { 1232 final Bundle arguments = new Bundle(); 1233 arguments.putParcelable("response", response); 1234 return recordEventInternal("onInlineSuggestionsResponse", supplier::getAsBoolean, 1235 arguments); 1236 } 1237 1238 void onInlineSuggestionClickedEvent(@NonNull Runnable runnable) { 1239 final Bundle arguments = new Bundle(); 1240 recordEventInternal("onInlineSuggestionClickedEvent", runnable, arguments); 1241 } 1242 1243 void onInlineSuggestionLongClickedEvent(@NonNull Runnable runnable) { 1244 final Bundle arguments = new Bundle(); 1245 recordEventInternal("onInlineSuggestionLongClickedEvent", runnable, arguments); 1246 } 1247 } 1248 } 1249