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.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; 20 21 import static com.android.cts.mockime.MockImeSession.MOCK_IME_SETTINGS_FILE; 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.res.Configuration; 29 import android.inputmethodservice.InputMethodService; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.os.HandlerThread; 33 import android.os.IBinder; 34 import android.os.Looper; 35 import android.os.Parcel; 36 import android.os.Process; 37 import android.os.ResultReceiver; 38 import android.os.SystemClock; 39 import androidx.annotation.AnyThread; 40 import androidx.annotation.CallSuper; 41 import androidx.annotation.NonNull; 42 import androidx.annotation.Nullable; 43 import androidx.annotation.WorkerThread; 44 import android.text.TextUtils; 45 import android.util.Log; 46 import android.util.TypedValue; 47 import android.view.Gravity; 48 import android.view.KeyEvent; 49 import android.view.View; 50 import android.view.Window; 51 import android.view.WindowInsets; 52 import android.view.inputmethod.EditorInfo; 53 import android.view.inputmethod.InputBinding; 54 import android.view.inputmethod.InputMethod; 55 import android.widget.LinearLayout; 56 import android.widget.RelativeLayout; 57 import android.widget.TextView; 58 59 import java.io.IOException; 60 import java.io.InputStream; 61 import java.util.concurrent.atomic.AtomicReference; 62 import java.util.function.BooleanSupplier; 63 import java.util.function.Consumer; 64 import java.util.function.Supplier; 65 66 /** 67 * Mock IME for end-to-end tests. 68 */ 69 public final class MockIme extends InputMethodService { 70 71 private static final String TAG = "MockIme"; 72 getComponentName(@onNull String packageName)73 static ComponentName getComponentName(@NonNull String packageName) { 74 return new ComponentName(packageName, MockIme.class.getName()); 75 } 76 getImeId(@onNull String packageName)77 static String getImeId(@NonNull String packageName) { 78 return new ComponentName(packageName, MockIme.class.getName()).flattenToShortString(); 79 } 80 getCommandActionName(@onNull String eventActionName)81 static String getCommandActionName(@NonNull String eventActionName) { 82 return eventActionName + ".command"; 83 } 84 85 private final HandlerThread mHandlerThread = new HandlerThread("CommandReceiver"); 86 87 private final Handler mMainHandler = new Handler(); 88 89 private static final class CommandReceiver extends BroadcastReceiver { 90 @NonNull 91 private final String mActionName; 92 @NonNull 93 private final Consumer<ImeCommand> mOnReceiveCommand; 94 CommandReceiver(@onNull String actionName, @NonNull Consumer<ImeCommand> onReceiveCommand)95 CommandReceiver(@NonNull String actionName, 96 @NonNull Consumer<ImeCommand> onReceiveCommand) { 97 mActionName = actionName; 98 mOnReceiveCommand = onReceiveCommand; 99 } 100 101 @Override onReceive(Context context, Intent intent)102 public void onReceive(Context context, Intent intent) { 103 if (TextUtils.equals(mActionName, intent.getAction())) { 104 mOnReceiveCommand.accept(ImeCommand.fromBundle(intent.getExtras())); 105 } 106 } 107 } 108 109 @WorkerThread onReceiveCommand(@onNull ImeCommand command)110 private void onReceiveCommand(@NonNull ImeCommand command) { 111 getTracer().onReceiveCommand(command, () -> { 112 if (command.shouldDispatchToMainThread()) { 113 mMainHandler.post(() -> onHandleCommand(command)); 114 } else { 115 onHandleCommand(command); 116 } 117 }); 118 } 119 120 @AnyThread onHandleCommand(@onNull ImeCommand command)121 private void onHandleCommand(@NonNull ImeCommand command) { 122 getTracer().onHandleCommand(command, () -> { 123 if (command.shouldDispatchToMainThread()) { 124 if (Looper.myLooper() != Looper.getMainLooper()) { 125 throw new IllegalStateException("command " + command 126 + " should be handled on the main thread"); 127 } 128 switch (command.getName()) { 129 case "commitText": { 130 final CharSequence text = command.getExtras().getString("text"); 131 final int newCursorPosition = 132 command.getExtras().getInt("newCursorPosition"); 133 getCurrentInputConnection().commitText(text, newCursorPosition); 134 break; 135 } 136 case "setBackDisposition": { 137 final int backDisposition = 138 command.getExtras().getInt("backDisposition"); 139 setBackDisposition(backDisposition); 140 break; 141 } 142 case "requestHideSelf": { 143 final int flags = command.getExtras().getInt("flags"); 144 requestHideSelf(flags); 145 break; 146 } 147 case "requestShowSelf": { 148 final int flags = command.getExtras().getInt("flags"); 149 requestShowSelf(flags); 150 break; 151 } 152 } 153 } 154 }); 155 } 156 157 @Nullable 158 private CommandReceiver mCommandReceiver; 159 160 @Nullable 161 private ImeSettings mSettings; 162 163 private final AtomicReference<String> mImeEventActionName = new AtomicReference<>(); 164 165 @Nullable getImeEventActionName()166 String getImeEventActionName() { 167 return mImeEventActionName.get(); 168 } 169 170 private class MockInputMethodImpl extends InputMethodImpl { 171 @Override showSoftInput(int flags, ResultReceiver resultReceiver)172 public void showSoftInput(int flags, ResultReceiver resultReceiver) { 173 getTracer().showSoftInput(flags, resultReceiver, 174 () -> super.showSoftInput(flags, resultReceiver)); 175 } 176 177 @Override hideSoftInput(int flags, ResultReceiver resultReceiver)178 public void hideSoftInput(int flags, ResultReceiver resultReceiver) { 179 getTracer().hideSoftInput(flags, resultReceiver, 180 () -> super.hideSoftInput(flags, resultReceiver)); 181 } 182 183 @Override attachToken(IBinder token)184 public void attachToken(IBinder token) { 185 getTracer().attachToken(token, () -> super.attachToken(token)); 186 } 187 188 @Override bindInput(InputBinding binding)189 public void bindInput(InputBinding binding) { 190 getTracer().bindInput(binding, () -> super.bindInput(binding)); 191 } 192 193 @Override unbindInput()194 public void unbindInput() { 195 getTracer().unbindInput(() -> super.unbindInput()); 196 } 197 } 198 199 @Nullable readSettings()200 private ImeSettings readSettings() { 201 try (InputStream is = openFileInput(MOCK_IME_SETTINGS_FILE)) { 202 Parcel parcel = null; 203 try { 204 parcel = Parcel.obtain(); 205 final byte[] buffer = new byte[4096]; 206 while (true) { 207 final int numRead = is.read(buffer); 208 if (numRead <= 0) { 209 break; 210 } 211 parcel.unmarshall(buffer, 0, numRead); 212 } 213 parcel.setDataPosition(0); 214 return new ImeSettings(parcel); 215 } finally { 216 if (parcel != null) { 217 parcel.recycle(); 218 } 219 } 220 } catch (IOException e) { 221 } 222 return null; 223 } 224 225 @Override onCreate()226 public void onCreate() { 227 // Initialize minimum settings to send events in Tracer#onCreate(). 228 mSettings = readSettings(); 229 if (mSettings == null) { 230 throw new IllegalStateException("Settings file is not found. " 231 + "Make sure MockImeSession.create() is used to launch Mock IME."); 232 } 233 mImeEventActionName.set(mSettings.getEventCallbackActionName()); 234 235 getTracer().onCreate(() -> { 236 super.onCreate(); 237 mHandlerThread.start(); 238 final String actionName = getCommandActionName(mSettings.getEventCallbackActionName()); 239 mCommandReceiver = new CommandReceiver(actionName, this::onReceiveCommand); 240 registerReceiver(mCommandReceiver, 241 new IntentFilter(actionName), null /* broadcastPermission */, 242 new Handler(mHandlerThread.getLooper())); 243 244 final int windowFlags = mSettings.getWindowFlags(0); 245 final int windowFlagsMask = mSettings.getWindowFlagsMask(0); 246 if (windowFlags != 0 || windowFlagsMask != 0) { 247 final int prevFlags = getWindow().getWindow().getAttributes().flags; 248 getWindow().getWindow().setFlags(windowFlags, windowFlagsMask); 249 // For some reasons, seems that we need to post another requestLayout() when 250 // FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS bit is changed. 251 // TODO: Investigate the reason. 252 if ((windowFlagsMask & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0) { 253 final boolean hadFlag = (prevFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0; 254 final boolean hasFlag = (windowFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0; 255 if (hadFlag != hasFlag) { 256 final View decorView = getWindow().getWindow().getDecorView(); 257 decorView.post(() -> decorView.requestLayout()); 258 } 259 } 260 } 261 262 if (mSettings.hasNavigationBarColor()) { 263 getWindow().getWindow().setNavigationBarColor(mSettings.getNavigationBarColor()); 264 } 265 }); 266 } 267 268 @Override onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly)269 public void onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly) { 270 getTracer().onConfigureWindow(win, isFullscreen, isCandidatesOnly, 271 () -> super.onConfigureWindow(win, isFullscreen, isCandidatesOnly)); 272 } 273 274 @Override onEvaluateFullscreenMode()275 public boolean onEvaluateFullscreenMode() { 276 return getTracer().onEvaluateFullscreenMode(() -> 277 mSettings.fullscreenModeAllowed(false) && super.onEvaluateFullscreenMode()); 278 } 279 280 private static final class KeyboardLayoutView extends LinearLayout { 281 @NonNull 282 private final ImeSettings mSettings; 283 @NonNull 284 private final View.OnLayoutChangeListener mLayoutListener; 285 KeyboardLayoutView(Context context, @NonNull ImeSettings imeSettings, @Nullable Consumer<ImeLayoutInfo> onInputViewLayoutChangedCallback)286 KeyboardLayoutView(Context context, @NonNull ImeSettings imeSettings, 287 @Nullable Consumer<ImeLayoutInfo> onInputViewLayoutChangedCallback) { 288 super(context); 289 290 mSettings = imeSettings; 291 292 setOrientation(VERTICAL); 293 294 final int defaultBackgroundColor = 295 getResources().getColor(android.R.color.holo_orange_dark, null); 296 setBackgroundColor(mSettings.getBackgroundColor(defaultBackgroundColor)); 297 298 final int mainSpacerHeight = mSettings.getInputViewHeightWithoutSystemWindowInset( 299 LayoutParams.WRAP_CONTENT); 300 { 301 final RelativeLayout layout = new RelativeLayout(getContext()); 302 final TextView textView = new TextView(getContext()); 303 final RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( 304 RelativeLayout.LayoutParams.MATCH_PARENT, 305 RelativeLayout.LayoutParams.WRAP_CONTENT); 306 params.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE); 307 textView.setLayoutParams(params); 308 textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); 309 textView.setGravity(Gravity.CENTER); 310 textView.setText(getImeId(getContext().getPackageName())); 311 layout.addView(textView); 312 addView(layout, LayoutParams.MATCH_PARENT, mainSpacerHeight); 313 } 314 315 final int systemUiVisibility = mSettings.getInputViewSystemUiVisibility(0); 316 if (systemUiVisibility != 0) { 317 setSystemUiVisibility(systemUiVisibility); 318 } 319 320 mLayoutListener = (View v, int left, int top, int right, int bottom, int oldLeft, 321 int oldTop, int oldRight, int oldBottom) -> 322 onInputViewLayoutChangedCallback.accept( 323 ImeLayoutInfo.fromLayoutListenerCallback( 324 v, left, top, right, bottom, oldLeft, oldTop, oldRight, 325 oldBottom)); 326 this.addOnLayoutChangeListener(mLayoutListener); 327 } 328 updateBottomPaddingIfNecessary(int newPaddingBottom)329 private void updateBottomPaddingIfNecessary(int newPaddingBottom) { 330 if (getPaddingBottom() != newPaddingBottom) { 331 setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), newPaddingBottom); 332 } 333 } 334 335 @Override onApplyWindowInsets(WindowInsets insets)336 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 337 if (insets.isConsumed() 338 || (getSystemUiVisibility() & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0) { 339 // In this case we are not interested in consuming NavBar region. 340 // Make sure that the bottom padding is empty. 341 updateBottomPaddingIfNecessary(0); 342 return insets; 343 } 344 345 // In some cases the bottom system window inset is not a navigation bar. Wear devices 346 // that have bottom chin are examples. For now, assume that it's a navigation bar if it 347 // has the same height as the root window's stable bottom inset. 348 final WindowInsets rootWindowInsets = getRootWindowInsets(); 349 if (rootWindowInsets != null && (rootWindowInsets.getStableInsetBottom() 350 != insets.getSystemWindowInsetBottom())) { 351 // This is probably not a NavBar. 352 updateBottomPaddingIfNecessary(0); 353 return insets; 354 } 355 356 final int possibleNavBarHeight = insets.getSystemWindowInsetBottom(); 357 updateBottomPaddingIfNecessary(possibleNavBarHeight); 358 return possibleNavBarHeight <= 0 359 ? insets 360 : insets.replaceSystemWindowInsets( 361 insets.getSystemWindowInsetLeft(), 362 insets.getSystemWindowInsetTop(), 363 insets.getSystemWindowInsetRight(), 364 0 /* bottom */); 365 } 366 367 @Override onDetachedFromWindow()368 protected void onDetachedFromWindow() { 369 super.onDetachedFromWindow(); 370 removeOnLayoutChangeListener(mLayoutListener); 371 } 372 } 373 onInputViewLayoutChanged(@onNull ImeLayoutInfo layoutInfo)374 private void onInputViewLayoutChanged(@NonNull ImeLayoutInfo layoutInfo) { 375 getTracer().onInputViewLayoutChanged(layoutInfo, () -> { }); 376 } 377 378 @Override onCreateInputView()379 public View onCreateInputView() { 380 return getTracer().onCreateInputView(() -> 381 new KeyboardLayoutView(this, mSettings, this::onInputViewLayoutChanged)); 382 } 383 384 @Override onStartInput(EditorInfo editorInfo, boolean restarting)385 public void onStartInput(EditorInfo editorInfo, boolean restarting) { 386 getTracer().onStartInput(editorInfo, restarting, 387 () -> super.onStartInput(editorInfo, restarting)); 388 } 389 390 @Override onStartInputView(EditorInfo editorInfo, boolean restarting)391 public void onStartInputView(EditorInfo editorInfo, boolean restarting) { 392 getTracer().onStartInputView(editorInfo, restarting, 393 () -> super.onStartInputView(editorInfo, restarting)); 394 } 395 396 @Override onFinishInputView(boolean finishingInput)397 public void onFinishInputView(boolean finishingInput) { 398 getTracer().onFinishInputView(finishingInput, 399 () -> super.onFinishInputView(finishingInput)); 400 } 401 402 @Override onFinishInput()403 public void onFinishInput() { 404 getTracer().onFinishInput(() -> super.onFinishInput()); 405 } 406 407 @Override onKeyDown(int keyCode, KeyEvent event)408 public boolean onKeyDown(int keyCode, KeyEvent event) { 409 return getTracer().onKeyDown(keyCode, event, () -> super.onKeyDown(keyCode, event)); 410 } 411 412 @CallSuper onEvaluateInputViewShown()413 public boolean onEvaluateInputViewShown() { 414 return getTracer().onEvaluateInputViewShown(() -> { 415 // onShowInputRequested() is indeed @CallSuper so we always call this, even when the 416 // result is ignored. 417 final boolean originalResult = super.onEvaluateInputViewShown(); 418 if (!mSettings.getHardKeyboardConfigurationBehaviorAllowed(false)) { 419 final Configuration config = getResources().getConfiguration(); 420 if (config.keyboard != Configuration.KEYBOARD_NOKEYS 421 && config.hardKeyboardHidden != Configuration.HARDKEYBOARDHIDDEN_YES) { 422 // Override the behavior of InputMethodService#onEvaluateInputViewShown() 423 return true; 424 } 425 } 426 return originalResult; 427 }); 428 } 429 430 @Override onShowInputRequested(int flags, boolean configChange)431 public boolean onShowInputRequested(int flags, boolean configChange) { 432 return getTracer().onShowInputRequested(flags, configChange, () -> { 433 // onShowInputRequested() is not marked with @CallSuper, but just in case. 434 final boolean originalResult = super.onShowInputRequested(flags, configChange); 435 if (!mSettings.getHardKeyboardConfigurationBehaviorAllowed(false)) { 436 if ((flags & InputMethod.SHOW_EXPLICIT) == 0 437 && getResources().getConfiguration().keyboard 438 != Configuration.KEYBOARD_NOKEYS) { 439 // Override the behavior of InputMethodService#onShowInputRequested() 440 return true; 441 } 442 } 443 return originalResult; 444 }); 445 } 446 447 @Override 448 public void onDestroy() { 449 getTracer().onDestroy(() -> { 450 super.onDestroy(); 451 unregisterReceiver(mCommandReceiver); 452 mHandlerThread.quitSafely(); 453 }); 454 } 455 456 @Override 457 public AbstractInputMethodImpl onCreateInputMethodInterface() { 458 return getTracer().onCreateInputMethodInterface(() -> new MockInputMethodImpl()); 459 } 460 461 private final ThreadLocal<Tracer> mThreadLocalTracer = new ThreadLocal<>(); 462 463 private Tracer getTracer() { 464 Tracer tracer = mThreadLocalTracer.get(); 465 if (tracer == null) { 466 tracer = new Tracer(this); 467 mThreadLocalTracer.set(tracer); 468 } 469 return tracer; 470 } 471 472 @NonNull 473 private ImeState getState() { 474 final boolean hasInputBinding = getCurrentInputBinding() != null; 475 final boolean hasDummyInputConnectionConnection = 476 !hasInputBinding 477 || getCurrentInputConnection() == getCurrentInputBinding().getConnection(); 478 return new ImeState(hasInputBinding, hasDummyInputConnectionConnection); 479 } 480 481 /** 482 * Event tracing helper class for {@link MockIme}. 483 */ 484 private static final class Tracer { 485 486 @NonNull 487 private final MockIme mIme; 488 489 private final int mThreadId = Process.myTid(); 490 491 @NonNull 492 private final String mThreadName = 493 Thread.currentThread().getName() != null ? Thread.currentThread().getName() : ""; 494 495 private final boolean mIsMainThread = 496 Looper.getMainLooper().getThread() == Thread.currentThread(); 497 498 private int mNestLevel = 0; 499 500 private String mImeEventActionName; 501 502 Tracer(@NonNull MockIme mockIme) { 503 mIme = mockIme; 504 } 505 506 private void sendEventInternal(@NonNull ImeEvent event) { 507 final Intent intent = new Intent(); 508 intent.setPackage(mIme.getPackageName()); 509 if (mImeEventActionName == null) { 510 mImeEventActionName = mIme.getImeEventActionName(); 511 } 512 if (mImeEventActionName == null) { 513 Log.e(TAG, "Tracer cannot be used before onCreate()"); 514 return; 515 } 516 intent.setAction(mImeEventActionName); 517 intent.putExtras(event.toBundle()); 518 mIme.sendBroadcast(intent); 519 } 520 521 private void recordEventInternal(@NonNull String eventName, @NonNull Runnable runnable) { 522 recordEventInternal(eventName, runnable, new Bundle()); 523 } 524 525 private void recordEventInternal(@NonNull String eventName, @NonNull Runnable runnable, 526 @NonNull Bundle arguments) { 527 recordEventInternal(eventName, () -> { 528 runnable.run(); return null; 529 }, arguments); 530 } 531 532 private <T> T recordEventInternal(@NonNull String eventName, 533 @NonNull Supplier<T> supplier) { 534 return recordEventInternal(eventName, supplier, new Bundle()); 535 } 536 537 private <T> T recordEventInternal(@NonNull String eventName, 538 @NonNull Supplier<T> supplier, @NonNull Bundle arguments) { 539 final ImeState enterState = mIme.getState(); 540 final long enterTimestamp = SystemClock.elapsedRealtimeNanos(); 541 final long enterWallTime = System.currentTimeMillis(); 542 final int nestLevel = mNestLevel; 543 // Send enter event 544 sendEventInternal(new ImeEvent(eventName, nestLevel, mThreadName, 545 mThreadId, mIsMainThread, enterTimestamp, 0, enterWallTime, 546 0, enterState, null, arguments, null)); 547 ++mNestLevel; 548 T result; 549 try { 550 result = supplier.get(); 551 } finally { 552 --mNestLevel; 553 } 554 final long exitTimestamp = SystemClock.elapsedRealtimeNanos(); 555 final long exitWallTime = System.currentTimeMillis(); 556 final ImeState exitState = mIme.getState(); 557 // Send exit event 558 sendEventInternal(new ImeEvent(eventName, nestLevel, mThreadName, 559 mThreadId, mIsMainThread, enterTimestamp, exitTimestamp, enterWallTime, 560 exitWallTime, enterState, exitState, arguments, result)); 561 return result; 562 } 563 564 public void onCreate(@NonNull Runnable runnable) { 565 recordEventInternal("onCreate", runnable); 566 } 567 568 public void onConfigureWindow(Window win, boolean isFullscreen, 569 boolean isCandidatesOnly, @NonNull Runnable runnable) { 570 final Bundle arguments = new Bundle(); 571 arguments.putBoolean("isFullscreen", isFullscreen); 572 arguments.putBoolean("isCandidatesOnly", isCandidatesOnly); 573 recordEventInternal("onConfigureWindow", runnable, arguments); 574 } 575 576 public boolean onEvaluateFullscreenMode(@NonNull BooleanSupplier supplier) { 577 return recordEventInternal("onEvaluateFullscreenMode", supplier::getAsBoolean); 578 } 579 580 public boolean onEvaluateInputViewShown(@NonNull BooleanSupplier supplier) { 581 return recordEventInternal("onEvaluateInputViewShown", supplier::getAsBoolean); 582 } 583 584 public View onCreateInputView(@NonNull Supplier<View> supplier) { 585 return recordEventInternal("onCreateInputView", supplier); 586 } 587 588 public void onStartInput(EditorInfo editorInfo, boolean restarting, 589 @NonNull Runnable runnable) { 590 final Bundle arguments = new Bundle(); 591 arguments.putParcelable("editorInfo", editorInfo); 592 arguments.putBoolean("restarting", restarting); 593 recordEventInternal("onStartInput", runnable, arguments); 594 } 595 596 public void onStartInputView(EditorInfo editorInfo, boolean restarting, 597 @NonNull Runnable runnable) { 598 final Bundle arguments = new Bundle(); 599 arguments.putParcelable("editorInfo", editorInfo); 600 arguments.putBoolean("restarting", restarting); 601 recordEventInternal("onStartInputView", runnable, arguments); 602 } 603 604 public void onFinishInputView(boolean finishingInput, @NonNull Runnable runnable) { 605 final Bundle arguments = new Bundle(); 606 arguments.putBoolean("finishingInput", finishingInput); 607 recordEventInternal("onFinishInputView", runnable, arguments); 608 } 609 610 public void onFinishInput(@NonNull Runnable runnable) { 611 recordEventInternal("onFinishInput", runnable); 612 } 613 614 public boolean onKeyDown(int keyCode, KeyEvent event, @NonNull BooleanSupplier supplier) { 615 final Bundle arguments = new Bundle(); 616 arguments.putInt("keyCode", keyCode); 617 arguments.putParcelable("event", event); 618 return recordEventInternal("onKeyDown", supplier::getAsBoolean, arguments); 619 } 620 621 public boolean onShowInputRequested(int flags, boolean configChange, 622 @NonNull BooleanSupplier supplier) { 623 final Bundle arguments = new Bundle(); 624 arguments.putInt("flags", flags); 625 arguments.putBoolean("configChange", configChange); 626 return recordEventInternal("onShowInputRequested", supplier::getAsBoolean, arguments); 627 } 628 629 public void onDestroy(@NonNull Runnable runnable) { 630 recordEventInternal("onDestroy", runnable); 631 } 632 633 public void attachToken(IBinder token, @NonNull Runnable runnable) { 634 final Bundle arguments = new Bundle(); 635 arguments.putBinder("token", token); 636 recordEventInternal("attachToken", runnable, arguments); 637 } 638 639 public void bindInput(InputBinding binding, @NonNull Runnable runnable) { 640 final Bundle arguments = new Bundle(); 641 arguments.putParcelable("binding", binding); 642 recordEventInternal("bindInput", runnable, arguments); 643 } 644 645 public void unbindInput(@NonNull Runnable runnable) { 646 recordEventInternal("unbindInput", runnable); 647 } 648 649 public void showSoftInput(int flags, ResultReceiver resultReceiver, 650 @NonNull Runnable runnable) { 651 final Bundle arguments = new Bundle(); 652 arguments.putInt("flags", flags); 653 arguments.putParcelable("resultReceiver", resultReceiver); 654 recordEventInternal("showSoftInput", runnable, arguments); 655 } 656 657 public void hideSoftInput(int flags, ResultReceiver resultReceiver, 658 @NonNull Runnable runnable) { 659 final Bundle arguments = new Bundle(); 660 arguments.putInt("flags", flags); 661 arguments.putParcelable("resultReceiver", resultReceiver); 662 recordEventInternal("hideSoftInput", runnable, arguments); 663 } 664 665 public AbstractInputMethodImpl onCreateInputMethodInterface( 666 @NonNull Supplier<AbstractInputMethodImpl> supplier) { 667 return recordEventInternal("onCreateInputMethodInterface", supplier); 668 } 669 670 public void onReceiveCommand( 671 @NonNull ImeCommand command, @NonNull Runnable runnable) { 672 final Bundle arguments = new Bundle(); 673 arguments.putBundle("command", command.toBundle()); 674 recordEventInternal("onReceiveCommand", runnable, arguments); 675 } 676 677 public void onHandleCommand( 678 @NonNull ImeCommand command, @NonNull Runnable runnable) { 679 final Bundle arguments = new Bundle(); 680 arguments.putBundle("command", command.toBundle()); 681 recordEventInternal("onHandleCommand", runnable, arguments); 682 } 683 684 public void onInputViewLayoutChanged(@NonNull ImeLayoutInfo imeLayoutInfo, 685 @NonNull Runnable runnable) { 686 final Bundle arguments = new Bundle(); 687 imeLayoutInfo.writeToBundle(arguments); 688 recordEventInternal("onInputViewLayoutChanged", runnable, arguments); 689 } 690 } 691 } 692