• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.cts.mockime;
18 
19 import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
20 
21 import android.content.BroadcastReceiver;
22 import android.content.ComponentName;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.content.res.Configuration;
27 import android.inputmethodservice.InputMethodService;
28 import android.os.Build;
29 import android.os.Bundle;
30 import android.os.Handler;
31 import android.os.HandlerThread;
32 import android.os.IBinder;
33 import android.os.Looper;
34 import android.os.Process;
35 import android.os.ResultReceiver;
36 import android.os.SystemClock;
37 import android.text.TextUtils;
38 import android.util.Log;
39 import android.util.TypedValue;
40 import android.view.Gravity;
41 import android.view.KeyEvent;
42 import android.view.View;
43 import android.view.Window;
44 import android.view.WindowInsets;
45 import android.view.WindowManager;
46 import android.view.inputmethod.CompletionInfo;
47 import android.view.inputmethod.CorrectionInfo;
48 import android.view.inputmethod.CursorAnchorInfo;
49 import android.view.inputmethod.EditorInfo;
50 import android.view.inputmethod.ExtractedTextRequest;
51 import android.view.inputmethod.InputBinding;
52 import android.view.inputmethod.InputContentInfo;
53 import android.view.inputmethod.InputMethod;
54 import android.widget.LinearLayout;
55 import android.widget.RelativeLayout;
56 import android.widget.TextView;
57 
58 import androidx.annotation.AnyThread;
59 import androidx.annotation.CallSuper;
60 import androidx.annotation.NonNull;
61 import androidx.annotation.Nullable;
62 import androidx.annotation.WorkerThread;
63 
64 import java.util.concurrent.atomic.AtomicReference;
65 import java.util.function.BooleanSupplier;
66 import java.util.function.Consumer;
67 import java.util.function.Supplier;
68 
69 /**
70  * Mock IME for end-to-end tests.
71  */
72 public final class MockIme extends InputMethodService {
73 
74     private static final String TAG = "MockIme";
75 
76     private static final String PACKAGE_NAME = "com.android.cts.mockime";
77 
getComponentName()78     static ComponentName getComponentName() {
79         return new ComponentName(PACKAGE_NAME, MockIme.class.getName());
80     }
81 
getImeId()82     static String getImeId() {
83         return getComponentName().flattenToShortString();
84     }
85 
getCommandActionName(@onNull String eventActionName)86     static String getCommandActionName(@NonNull String eventActionName) {
87         return eventActionName + ".command";
88     }
89 
90     private final HandlerThread mHandlerThread = new HandlerThread("CommandReceiver");
91 
92     private final Handler mMainHandler = new Handler();
93 
94     private static final class CommandReceiver extends BroadcastReceiver {
95         @NonNull
96         private final String mActionName;
97         @NonNull
98         private final Consumer<ImeCommand> mOnReceiveCommand;
99 
CommandReceiver(@onNull String actionName, @NonNull Consumer<ImeCommand> onReceiveCommand)100         CommandReceiver(@NonNull String actionName,
101                 @NonNull Consumer<ImeCommand> onReceiveCommand) {
102             mActionName = actionName;
103             mOnReceiveCommand = onReceiveCommand;
104         }
105 
106         @Override
onReceive(Context context, Intent intent)107         public void onReceive(Context context, Intent intent) {
108             if (TextUtils.equals(mActionName, intent.getAction())) {
109                 mOnReceiveCommand.accept(ImeCommand.fromBundle(intent.getExtras()));
110             }
111         }
112     }
113 
114     @WorkerThread
onReceiveCommand(@onNull ImeCommand command)115     private void onReceiveCommand(@NonNull ImeCommand command) {
116         getTracer().onReceiveCommand(command, () -> {
117             if (command.shouldDispatchToMainThread()) {
118                 mMainHandler.post(() -> onHandleCommand(command));
119             } else {
120                 onHandleCommand(command);
121             }
122         });
123     }
124 
125     @AnyThread
onHandleCommand(@onNull ImeCommand command)126     private void onHandleCommand(@NonNull ImeCommand command) {
127         getTracer().onHandleCommand(command, () -> {
128             if (command.shouldDispatchToMainThread()) {
129                 if (Looper.myLooper() != Looper.getMainLooper()) {
130                     throw new IllegalStateException("command " + command
131                             + " should be handled on the main thread");
132                 }
133                 switch (command.getName()) {
134                     case "getTextBeforeCursor": {
135                         final int n = command.getExtras().getInt("n");
136                         final int flag = command.getExtras().getInt("flag");
137                         return getCurrentInputConnection().getTextBeforeCursor(n, flag);
138                     }
139                     case "getTextAfterCursor": {
140                         final int n = command.getExtras().getInt("n");
141                         final int flag = command.getExtras().getInt("flag");
142                         return getCurrentInputConnection().getTextAfterCursor(n, flag);
143                     }
144                     case "getSelectedText": {
145                         final int flag = command.getExtras().getInt("flag");
146                         return getCurrentInputConnection().getSelectedText(flag);
147                     }
148                     case "getCursorCapsMode": {
149                         final int reqModes = command.getExtras().getInt("reqModes");
150                         return getCurrentInputConnection().getCursorCapsMode(reqModes);
151                     }
152                     case "getExtractedText": {
153                         final ExtractedTextRequest request =
154                                 command.getExtras().getParcelable("request");
155                         final int flags = command.getExtras().getInt("flags");
156                         return getCurrentInputConnection().getExtractedText(request, flags);
157                     }
158                     case "deleteSurroundingText": {
159                         final int beforeLength = command.getExtras().getInt("beforeLength");
160                         final int afterLength = command.getExtras().getInt("afterLength");
161                         return getCurrentInputConnection().deleteSurroundingText(
162                                 beforeLength, afterLength);
163                     }
164                     case "deleteSurroundingTextInCodePoints": {
165                         final int beforeLength = command.getExtras().getInt("beforeLength");
166                         final int afterLength = command.getExtras().getInt("afterLength");
167                         return getCurrentInputConnection().deleteSurroundingTextInCodePoints(
168                                 beforeLength, afterLength);
169                     }
170                     case "setComposingText": {
171                         final CharSequence text = command.getExtras().getCharSequence("text");
172                         final int newCursorPosition =
173                                 command.getExtras().getInt("newCursorPosition");
174                         return getCurrentInputConnection().setComposingText(
175                                 text, newCursorPosition);
176                     }
177                     case "setComposingRegion": {
178                         final int start = command.getExtras().getInt("start");
179                         final int end = command.getExtras().getInt("end");
180                         return getCurrentInputConnection().setComposingRegion(start, end);
181                     }
182                     case "finishComposingText":
183                         return getCurrentInputConnection().finishComposingText();
184                     case "commitText": {
185                         final CharSequence text = command.getExtras().getCharSequence("text");
186                         final int newCursorPosition =
187                                 command.getExtras().getInt("newCursorPosition");
188                         return getCurrentInputConnection().commitText(text, newCursorPosition);
189                     }
190                     case "commitCompletion": {
191                         final CompletionInfo text = command.getExtras().getParcelable("text");
192                         return getCurrentInputConnection().commitCompletion(text);
193                     }
194                     case "commitCorrection": {
195                         final CorrectionInfo correctionInfo =
196                                 command.getExtras().getParcelable("correctionInfo");
197                         return getCurrentInputConnection().commitCorrection(correctionInfo);
198                     }
199                     case "setSelection": {
200                         final int start = command.getExtras().getInt("start");
201                         final int end = command.getExtras().getInt("end");
202                         return getCurrentInputConnection().setSelection(start, end);
203                     }
204                     case "performEditorAction": {
205                         final int editorAction = command.getExtras().getInt("editorAction");
206                         return getCurrentInputConnection().performEditorAction(editorAction);
207                     }
208                     case "performContextMenuAction": {
209                         final int id = command.getExtras().getInt("id");
210                         return getCurrentInputConnection().performContextMenuAction(id);
211                     }
212                     case "beginBatchEdit":
213                         return getCurrentInputConnection().beginBatchEdit();
214                     case "endBatchEdit":
215                         return getCurrentInputConnection().endBatchEdit();
216                     case "sendKeyEvent": {
217                         final KeyEvent event = command.getExtras().getParcelable("event");
218                         return getCurrentInputConnection().sendKeyEvent(event);
219                     }
220                     case "clearMetaKeyStates": {
221                         final int states = command.getExtras().getInt("states");
222                         return getCurrentInputConnection().clearMetaKeyStates(states);
223                     }
224                     case "reportFullscreenMode": {
225                         final boolean enabled = command.getExtras().getBoolean("enabled");
226                         return getCurrentInputConnection().reportFullscreenMode(enabled);
227                     }
228                     case "performPrivateCommand": {
229                         final String action = command.getExtras().getString("action");
230                         final Bundle data = command.getExtras().getBundle("data");
231                         return getCurrentInputConnection().performPrivateCommand(action, data);
232                     }
233                     case "requestCursorUpdates": {
234                         final int cursorUpdateMode = command.getExtras().getInt("cursorUpdateMode");
235                         return getCurrentInputConnection().requestCursorUpdates(cursorUpdateMode);
236                     }
237                     case "getHandler":
238                         return getCurrentInputConnection().getHandler();
239                     case "closeConnection":
240                         getCurrentInputConnection().closeConnection();
241                         return ImeEvent.RETURN_VALUE_UNAVAILABLE;
242                     case "commitContent": {
243                         final InputContentInfo inputContentInfo =
244                                 command.getExtras().getParcelable("inputContentInfo");
245                         final int flags = command.getExtras().getInt("flags");
246                         final Bundle opts = command.getExtras().getBundle("opts");
247                         return getCurrentInputConnection().commitContent(
248                                 inputContentInfo, flags, opts);
249                     }
250                     case "setBackDisposition": {
251                         final int backDisposition =
252                                 command.getExtras().getInt("backDisposition");
253                         setBackDisposition(backDisposition);
254                         return ImeEvent.RETURN_VALUE_UNAVAILABLE;
255                     }
256                     case "requestHideSelf": {
257                         final int flags = command.getExtras().getInt("flags");
258                         requestHideSelf(flags);
259                         return ImeEvent.RETURN_VALUE_UNAVAILABLE;
260                     }
261                     case "requestShowSelf": {
262                         final int flags = command.getExtras().getInt("flags");
263                         requestShowSelf(flags);
264                         return ImeEvent.RETURN_VALUE_UNAVAILABLE;
265                     }
266                     case "sendDownUpKeyEvents": {
267                         final int keyEventCode = command.getExtras().getInt("keyEventCode");
268                         sendDownUpKeyEvents(keyEventCode);
269                         return ImeEvent.RETURN_VALUE_UNAVAILABLE;
270                     }
271                     case "getDisplayId":
272                         return getSystemService(WindowManager.class)
273                                 .getDefaultDisplay().getDisplayId();
274                 }
275             }
276             return ImeEvent.RETURN_VALUE_UNAVAILABLE;
277         });
278     }
279 
280     @Nullable
281     private CommandReceiver mCommandReceiver;
282 
283     @Nullable
284     private ImeSettings mSettings;
285 
286     private final AtomicReference<String> mImeEventActionName = new AtomicReference<>();
287 
288     @Nullable
getImeEventActionName()289     String getImeEventActionName() {
290         return mImeEventActionName.get();
291     }
292 
293     private final AtomicReference<String> mClientPackageName = new AtomicReference<>();
294 
295     @Nullable
getClientPackageName()296     String getClientPackageName() {
297         return mClientPackageName.get();
298     }
299 
300     private class MockInputMethodImpl extends InputMethodImpl {
301         @Override
showSoftInput(int flags, ResultReceiver resultReceiver)302         public void showSoftInput(int flags, ResultReceiver resultReceiver) {
303             getTracer().showSoftInput(flags, resultReceiver,
304                     () -> super.showSoftInput(flags, resultReceiver));
305         }
306 
307         @Override
hideSoftInput(int flags, ResultReceiver resultReceiver)308         public void hideSoftInput(int flags, ResultReceiver resultReceiver) {
309             getTracer().hideSoftInput(flags, resultReceiver,
310                     () -> super.hideSoftInput(flags, resultReceiver));
311         }
312 
313         @Override
attachToken(IBinder token)314         public void attachToken(IBinder token) {
315             getTracer().attachToken(token, () -> super.attachToken(token));
316         }
317 
318         @Override
bindInput(InputBinding binding)319         public void bindInput(InputBinding binding) {
320             getTracer().bindInput(binding, () -> super.bindInput(binding));
321         }
322 
323         @Override
unbindInput()324         public void unbindInput() {
325             getTracer().unbindInput(() -> super.unbindInput());
326         }
327     }
328 
329     @Override
onCreate()330     public void onCreate() {
331         // Initialize minimum settings to send events in Tracer#onCreate().
332         mSettings = SettingsProvider.getSettings();
333         if (mSettings == null) {
334             throw new IllegalStateException("Settings file is not found. "
335                     + "Make sure MockImeSession.create() is used to launch Mock IME.");
336         }
337         mClientPackageName.set(mSettings.getClientPackageName());
338         mImeEventActionName.set(mSettings.getEventCallbackActionName());
339 
340         getTracer().onCreate(() -> {
341             super.onCreate();
342             mHandlerThread.start();
343             final String actionName = getCommandActionName(mSettings.getEventCallbackActionName());
344             mCommandReceiver = new CommandReceiver(actionName, this::onReceiveCommand);
345             final IntentFilter filter = new IntentFilter(actionName);
346             final Handler handler = new Handler(mHandlerThread.getLooper());
347             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
348                 registerReceiver(mCommandReceiver, filter, null /* broadcastPermission */, handler,
349                         Context.RECEIVER_VISIBLE_TO_INSTANT_APPS);
350             } else {
351                 registerReceiver(mCommandReceiver, filter, null /* broadcastPermission */, handler);
352             }
353 
354             final int windowFlags = mSettings.getWindowFlags(0);
355             final int windowFlagsMask = mSettings.getWindowFlagsMask(0);
356             if (windowFlags != 0 || windowFlagsMask != 0) {
357                 final int prevFlags = getWindow().getWindow().getAttributes().flags;
358                 getWindow().getWindow().setFlags(windowFlags, windowFlagsMask);
359                 // For some reasons, seems that we need to post another requestLayout() when
360                 // FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS bit is changed.
361                 // TODO: Investigate the reason.
362                 if ((windowFlagsMask & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0) {
363                     final boolean hadFlag = (prevFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
364                     final boolean hasFlag = (windowFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
365                     if (hadFlag != hasFlag) {
366                         final View decorView = getWindow().getWindow().getDecorView();
367                         decorView.post(() -> decorView.requestLayout());
368                     }
369                 }
370             }
371 
372             // Ensuring bar contrast interferes with the tests.
373             getWindow().getWindow().setStatusBarContrastEnforced(false);
374             getWindow().getWindow().setNavigationBarContrastEnforced(false);
375 
376             if (mSettings.hasNavigationBarColor()) {
377                 getWindow().getWindow().setNavigationBarColor(mSettings.getNavigationBarColor());
378             }
379         });
380     }
381 
382     @Override
onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly)383     public void onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly) {
384         getTracer().onConfigureWindow(win, isFullscreen, isCandidatesOnly,
385                 () -> super.onConfigureWindow(win, isFullscreen, isCandidatesOnly));
386     }
387 
388     @Override
onEvaluateFullscreenMode()389     public boolean onEvaluateFullscreenMode() {
390         return getTracer().onEvaluateFullscreenMode(() ->
391                 mSettings.fullscreenModeAllowed(false) && super.onEvaluateFullscreenMode());
392     }
393 
394     private static final class KeyboardLayoutView extends LinearLayout {
395         @NonNull
396         private final ImeSettings mSettings;
397         @NonNull
398         private final View.OnLayoutChangeListener mLayoutListener;
399 
KeyboardLayoutView(Context context, @NonNull ImeSettings imeSettings, @Nullable Consumer<ImeLayoutInfo> onInputViewLayoutChangedCallback)400         KeyboardLayoutView(Context context, @NonNull ImeSettings imeSettings,
401                 @Nullable Consumer<ImeLayoutInfo> onInputViewLayoutChangedCallback) {
402             super(context);
403 
404             mSettings = imeSettings;
405 
406             setOrientation(VERTICAL);
407 
408             final int defaultBackgroundColor =
409                     getResources().getColor(android.R.color.holo_orange_dark, null);
410             setBackgroundColor(mSettings.getBackgroundColor(defaultBackgroundColor));
411 
412             final int mainSpacerHeight = mSettings.getInputViewHeightWithoutSystemWindowInset(
413                     LayoutParams.WRAP_CONTENT);
414             {
415                 final RelativeLayout layout = new RelativeLayout(getContext());
416                 final TextView textView = new TextView(getContext());
417                 final RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
418                         RelativeLayout.LayoutParams.MATCH_PARENT,
419                         RelativeLayout.LayoutParams.WRAP_CONTENT);
420                 params.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
421                 textView.setLayoutParams(params);
422                 textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
423                 textView.setGravity(Gravity.CENTER);
424                 textView.setText(getImeId());
425                 layout.addView(textView);
426                 addView(layout, LayoutParams.MATCH_PARENT, mainSpacerHeight);
427             }
428 
429             final int systemUiVisibility = mSettings.getInputViewSystemUiVisibility(0);
430             if (systemUiVisibility != 0) {
431                 setSystemUiVisibility(systemUiVisibility);
432             }
433 
434             mLayoutListener = (View v, int left, int top, int right, int bottom, int oldLeft,
435                     int oldTop, int oldRight, int oldBottom) ->
436                     onInputViewLayoutChangedCallback.accept(
437                             ImeLayoutInfo.fromLayoutListenerCallback(
438                                     v, left, top, right, bottom, oldLeft, oldTop, oldRight,
439                                     oldBottom));
440             this.addOnLayoutChangeListener(mLayoutListener);
441         }
442 
updateBottomPaddingIfNecessary(int newPaddingBottom)443         private void updateBottomPaddingIfNecessary(int newPaddingBottom) {
444             if (getPaddingBottom() != newPaddingBottom) {
445                 setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), newPaddingBottom);
446             }
447         }
448 
449         @Override
onApplyWindowInsets(WindowInsets insets)450         public WindowInsets onApplyWindowInsets(WindowInsets insets) {
451             if (insets.isConsumed()
452                     || (getSystemUiVisibility() & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0) {
453                 // In this case we are not interested in consuming NavBar region.
454                 // Make sure that the bottom padding is empty.
455                 updateBottomPaddingIfNecessary(0);
456                 return insets;
457             }
458 
459             // In some cases the bottom system window inset is not a navigation bar. Wear devices
460             // that have bottom chin are examples.  For now, assume that it's a navigation bar if it
461             // has the same height as the root window's stable bottom inset.
462             final WindowInsets rootWindowInsets = getRootWindowInsets();
463             if (rootWindowInsets != null && (rootWindowInsets.getStableInsetBottom()
464                     != insets.getSystemWindowInsetBottom())) {
465                 // This is probably not a NavBar.
466                 updateBottomPaddingIfNecessary(0);
467                 return insets;
468             }
469 
470             final int possibleNavBarHeight = insets.getSystemWindowInsetBottom();
471             updateBottomPaddingIfNecessary(possibleNavBarHeight);
472             return possibleNavBarHeight <= 0
473                     ? insets
474                     : insets.replaceSystemWindowInsets(
475                             insets.getSystemWindowInsetLeft(),
476                             insets.getSystemWindowInsetTop(),
477                             insets.getSystemWindowInsetRight(),
478                             0 /* bottom */);
479         }
480 
481         @Override
onDetachedFromWindow()482         protected void onDetachedFromWindow() {
483             super.onDetachedFromWindow();
484             removeOnLayoutChangeListener(mLayoutListener);
485         }
486     }
487 
onInputViewLayoutChanged(@onNull ImeLayoutInfo layoutInfo)488     private void onInputViewLayoutChanged(@NonNull ImeLayoutInfo layoutInfo) {
489         getTracer().onInputViewLayoutChanged(layoutInfo, () -> { });
490     }
491 
492     @Override
onCreateInputView()493     public View onCreateInputView() {
494         return getTracer().onCreateInputView(() ->
495                 new KeyboardLayoutView(this, mSettings, this::onInputViewLayoutChanged));
496     }
497 
498     @Override
onStartInput(EditorInfo editorInfo, boolean restarting)499     public void onStartInput(EditorInfo editorInfo, boolean restarting) {
500         getTracer().onStartInput(editorInfo, restarting,
501                 () -> super.onStartInput(editorInfo, restarting));
502     }
503 
504     @Override
onStartInputView(EditorInfo editorInfo, boolean restarting)505     public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
506         getTracer().onStartInputView(editorInfo, restarting,
507                 () -> super.onStartInputView(editorInfo, restarting));
508     }
509 
510     @Override
onFinishInputView(boolean finishingInput)511     public void onFinishInputView(boolean finishingInput) {
512         getTracer().onFinishInputView(finishingInput,
513                 () -> super.onFinishInputView(finishingInput));
514     }
515 
516     @Override
onFinishInput()517     public void onFinishInput() {
518         getTracer().onFinishInput(() -> super.onFinishInput());
519     }
520 
521     @Override
onKeyDown(int keyCode, KeyEvent event)522     public boolean onKeyDown(int keyCode, KeyEvent event) {
523         return getTracer().onKeyDown(keyCode, event, () -> super.onKeyDown(keyCode, event));
524     }
525 
526     @Override
onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo)527     public void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) {
528         getTracer().onUpdateCursorAnchorInfo(cursorAnchorInfo,
529                 () -> super.onUpdateCursorAnchorInfo(cursorAnchorInfo));
530     }
531 
532     @CallSuper
onEvaluateInputViewShown()533     public boolean onEvaluateInputViewShown() {
534         return getTracer().onEvaluateInputViewShown(() -> {
535             // onShowInputRequested() is indeed @CallSuper so we always call this, even when the
536             // result is ignored.
537             final boolean originalResult = super.onEvaluateInputViewShown();
538             if (!mSettings.getHardKeyboardConfigurationBehaviorAllowed(false)) {
539                 final Configuration config = getResources().getConfiguration();
540                 if (config.keyboard != Configuration.KEYBOARD_NOKEYS
541                         && config.hardKeyboardHidden != Configuration.HARDKEYBOARDHIDDEN_YES) {
542                     // Override the behavior of InputMethodService#onEvaluateInputViewShown()
543                     return true;
544                 }
545             }
546             return originalResult;
547         });
548     }
549 
550     @Override
onShowInputRequested(int flags, boolean configChange)551     public boolean onShowInputRequested(int flags, boolean configChange) {
552         return getTracer().onShowInputRequested(flags, configChange, () -> {
553             // onShowInputRequested() is not marked with @CallSuper, but just in case.
554             final boolean originalResult = super.onShowInputRequested(flags, configChange);
555             if (!mSettings.getHardKeyboardConfigurationBehaviorAllowed(false)) {
556                 if ((flags & InputMethod.SHOW_EXPLICIT) == 0
557                         && getResources().getConfiguration().keyboard
558                         != Configuration.KEYBOARD_NOKEYS) {
559                     // Override the behavior of InputMethodService#onShowInputRequested()
560                     return true;
561                 }
562             }
563             return originalResult;
564         });
565     }
566 
567     @Override
568     public void onDestroy() {
569         getTracer().onDestroy(() -> {
570             super.onDestroy();
571             unregisterReceiver(mCommandReceiver);
572             mHandlerThread.quitSafely();
573         });
574     }
575 
576     @Override
577     public AbstractInputMethodImpl onCreateInputMethodInterface() {
578         return getTracer().onCreateInputMethodInterface(() -> new MockInputMethodImpl());
579     }
580 
581     private final ThreadLocal<Tracer> mThreadLocalTracer = new ThreadLocal<>();
582 
583     private Tracer getTracer() {
584         Tracer tracer = mThreadLocalTracer.get();
585         if (tracer == null) {
586             tracer = new Tracer(this);
587             mThreadLocalTracer.set(tracer);
588         }
589         return tracer;
590     }
591 
592     @NonNull
593     private ImeState getState() {
594         final boolean hasInputBinding = getCurrentInputBinding() != null;
595         final boolean hasDummyInputConnectionConnection =
596                 !hasInputBinding
597                         || getCurrentInputConnection() == getCurrentInputBinding().getConnection();
598         return new ImeState(hasInputBinding, hasDummyInputConnectionConnection);
599     }
600 
601     /**
602      * Event tracing helper class for {@link MockIme}.
603      */
604     private static final class Tracer {
605 
606         @NonNull
607         private final MockIme mIme;
608 
609         private final int mThreadId = Process.myTid();
610 
611         @NonNull
612         private final String mThreadName =
613                 Thread.currentThread().getName() != null ? Thread.currentThread().getName() : "";
614 
615         private final boolean mIsMainThread =
616                 Looper.getMainLooper().getThread() == Thread.currentThread();
617 
618         private int mNestLevel = 0;
619 
620         private String mImeEventActionName;
621 
622         private String mClientPackageName;
623 
624         Tracer(@NonNull MockIme mockIme) {
625             mIme = mockIme;
626         }
627 
628         private void sendEventInternal(@NonNull ImeEvent event) {
629             if (mImeEventActionName == null) {
630                 mImeEventActionName = mIme.getImeEventActionName();
631             }
632             if (mClientPackageName == null) {
633                 mClientPackageName = mIme.getClientPackageName();
634             }
635             if (mImeEventActionName == null || mClientPackageName == null) {
636                 Log.e(TAG, "Tracer cannot be used before onCreate()");
637                 return;
638             }
639             final Intent intent = new Intent()
640                     .setAction(mImeEventActionName)
641                     .setPackage(mClientPackageName)
642                     .putExtras(event.toBundle())
643                     .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
644                             | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
645             mIme.sendBroadcast(intent);
646         }
647 
648         private void recordEventInternal(@NonNull String eventName, @NonNull Runnable runnable) {
649             recordEventInternal(eventName, runnable, new Bundle());
650         }
651 
652         private void recordEventInternal(@NonNull String eventName, @NonNull Runnable runnable,
653                 @NonNull Bundle arguments) {
654             recordEventInternal(eventName, () -> {
655                 runnable.run(); return ImeEvent.RETURN_VALUE_UNAVAILABLE;
656             }, arguments);
657         }
658 
659         private <T> T recordEventInternal(@NonNull String eventName,
660                 @NonNull Supplier<T> supplier) {
661             return recordEventInternal(eventName, supplier, new Bundle());
662         }
663 
664         private <T> T recordEventInternal(@NonNull String eventName,
665                 @NonNull Supplier<T> supplier, @NonNull Bundle arguments) {
666             final ImeState enterState = mIme.getState();
667             final long enterTimestamp = SystemClock.elapsedRealtimeNanos();
668             final long enterWallTime = System.currentTimeMillis();
669             final int nestLevel = mNestLevel;
670             // Send enter event
671             sendEventInternal(new ImeEvent(eventName, nestLevel, mThreadName,
672                     mThreadId, mIsMainThread, enterTimestamp, 0, enterWallTime,
673                     0, enterState, null, arguments,
674                     ImeEvent.RETURN_VALUE_UNAVAILABLE));
675             ++mNestLevel;
676             T result;
677             try {
678                 result = supplier.get();
679             } finally {
680                 --mNestLevel;
681             }
682             final long exitTimestamp = SystemClock.elapsedRealtimeNanos();
683             final long exitWallTime = System.currentTimeMillis();
684             final ImeState exitState = mIme.getState();
685             // Send exit event
686             sendEventInternal(new ImeEvent(eventName, nestLevel, mThreadName,
687                     mThreadId, mIsMainThread, enterTimestamp, exitTimestamp, enterWallTime,
688                     exitWallTime, enterState, exitState, arguments, result));
689             return result;
690         }
691 
692         public void onCreate(@NonNull Runnable runnable) {
693             recordEventInternal("onCreate", runnable);
694         }
695 
696         public void onConfigureWindow(Window win, boolean isFullscreen,
697                 boolean isCandidatesOnly, @NonNull Runnable runnable) {
698             final Bundle arguments = new Bundle();
699             arguments.putBoolean("isFullscreen", isFullscreen);
700             arguments.putBoolean("isCandidatesOnly", isCandidatesOnly);
701             recordEventInternal("onConfigureWindow", runnable, arguments);
702         }
703 
704         public boolean onEvaluateFullscreenMode(@NonNull BooleanSupplier supplier) {
705             return recordEventInternal("onEvaluateFullscreenMode", supplier::getAsBoolean);
706         }
707 
708         public boolean onEvaluateInputViewShown(@NonNull BooleanSupplier supplier) {
709             return recordEventInternal("onEvaluateInputViewShown", supplier::getAsBoolean);
710         }
711 
712         public View onCreateInputView(@NonNull Supplier<View> supplier) {
713             return recordEventInternal("onCreateInputView", supplier);
714         }
715 
716         public void onStartInput(EditorInfo editorInfo, boolean restarting,
717                 @NonNull Runnable runnable) {
718             final Bundle arguments = new Bundle();
719             arguments.putParcelable("editorInfo", editorInfo);
720             arguments.putBoolean("restarting", restarting);
721             recordEventInternal("onStartInput", runnable, arguments);
722         }
723 
724         public void onStartInputView(EditorInfo editorInfo, boolean restarting,
725                 @NonNull Runnable runnable) {
726             final Bundle arguments = new Bundle();
727             arguments.putParcelable("editorInfo", editorInfo);
728             arguments.putBoolean("restarting", restarting);
729             recordEventInternal("onStartInputView", runnable, arguments);
730         }
731 
732         public void onFinishInputView(boolean finishingInput, @NonNull Runnable runnable) {
733             final Bundle arguments = new Bundle();
734             arguments.putBoolean("finishingInput", finishingInput);
735             recordEventInternal("onFinishInputView", runnable, arguments);
736         }
737 
738         public void onFinishInput(@NonNull Runnable runnable) {
739             recordEventInternal("onFinishInput", runnable);
740         }
741 
742         public boolean onKeyDown(int keyCode, KeyEvent event, @NonNull BooleanSupplier supplier) {
743             final Bundle arguments = new Bundle();
744             arguments.putInt("keyCode", keyCode);
745             arguments.putParcelable("event", event);
746             return recordEventInternal("onKeyDown", supplier::getAsBoolean, arguments);
747         }
748 
749         public void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo,
750                 @NonNull Runnable runnable) {
751             final Bundle arguments = new Bundle();
752             arguments.putParcelable("cursorAnchorInfo", cursorAnchorInfo);
753             recordEventInternal("onUpdateCursorAnchorInfo", runnable, arguments);
754         }
755 
756         public boolean onShowInputRequested(int flags, boolean configChange,
757                 @NonNull BooleanSupplier supplier) {
758             final Bundle arguments = new Bundle();
759             arguments.putInt("flags", flags);
760             arguments.putBoolean("configChange", configChange);
761             return recordEventInternal("onShowInputRequested", supplier::getAsBoolean, arguments);
762         }
763 
764         public void onDestroy(@NonNull Runnable runnable) {
765             recordEventInternal("onDestroy", runnable);
766         }
767 
768         public void attachToken(IBinder token, @NonNull Runnable runnable) {
769             final Bundle arguments = new Bundle();
770             arguments.putBinder("token", token);
771             recordEventInternal("attachToken", runnable, arguments);
772         }
773 
774         public void bindInput(InputBinding binding, @NonNull Runnable runnable) {
775             final Bundle arguments = new Bundle();
776             arguments.putParcelable("binding", binding);
777             recordEventInternal("bindInput", runnable, arguments);
778         }
779 
780         public void unbindInput(@NonNull Runnable runnable) {
781             recordEventInternal("unbindInput", runnable);
782         }
783 
784         public void showSoftInput(int flags, ResultReceiver resultReceiver,
785                 @NonNull Runnable runnable) {
786             final Bundle arguments = new Bundle();
787             arguments.putInt("flags", flags);
788             arguments.putParcelable("resultReceiver", resultReceiver);
789             recordEventInternal("showSoftInput", runnable, arguments);
790         }
791 
792         public void hideSoftInput(int flags, ResultReceiver resultReceiver,
793                 @NonNull Runnable runnable) {
794             final Bundle arguments = new Bundle();
795             arguments.putInt("flags", flags);
796             arguments.putParcelable("resultReceiver", resultReceiver);
797             recordEventInternal("hideSoftInput", runnable, arguments);
798         }
799 
800         public AbstractInputMethodImpl onCreateInputMethodInterface(
801                 @NonNull Supplier<AbstractInputMethodImpl> supplier) {
802             return recordEventInternal("onCreateInputMethodInterface", supplier);
803         }
804 
805         public void onReceiveCommand(
806                 @NonNull ImeCommand command, @NonNull Runnable runnable) {
807             final Bundle arguments = new Bundle();
808             arguments.putBundle("command", command.toBundle());
809             recordEventInternal("onReceiveCommand", runnable, arguments);
810         }
811 
812         public void onHandleCommand(
813                 @NonNull ImeCommand command, @NonNull Supplier<Object> resultSupplier) {
814             final Bundle arguments = new Bundle();
815             arguments.putBundle("command", command.toBundle());
816             recordEventInternal("onHandleCommand", resultSupplier, arguments);
817         }
818 
819         public void onInputViewLayoutChanged(@NonNull ImeLayoutInfo imeLayoutInfo,
820                 @NonNull Runnable runnable) {
821             final Bundle arguments = new Bundle();
822             imeLayoutInfo.writeToBundle(arguments);
823             recordEventInternal("onInputViewLayoutChanged", runnable, arguments);
824         }
825     }
826 }
827