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