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