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