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