Home | History | Annotate | Download | only in mockime
      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 
     78     static ComponentName getComponentName() {
     79         return new ComponentName(PACKAGE_NAME, MockIme.class.getName());
     80     }
     81 
     82     static String getImeId() {
     83         return getComponentName().flattenToShortString();
     84     }
     85 
     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 
    100         CommandReceiver(@NonNull String actionName,
    101                 @NonNull Consumer<ImeCommand> onReceiveCommand) {
    102             mActionName = actionName;
    103             mOnReceiveCommand = onReceiveCommand;
    104         }
    105 
    106         @Override
    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
    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
    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
    289     String getImeEventActionName() {
    290         return mImeEventActionName.get();
    291     }
    292 
    293     private final AtomicReference<String> mClientPackageName = new AtomicReference<>();
    294 
    295     @Nullable
    296     String getClientPackageName() {
    297         return mClientPackageName.get();
    298     }
    299 
    300     private class MockInputMethodImpl extends InputMethodImpl {
    301         @Override
    302         public void showSoftInput(int flags, ResultReceiver resultReceiver) {
    303             getTracer().showSoftInput(flags, resultReceiver,
    304                     () -> super.showSoftInput(flags, resultReceiver));
    305         }
    306 
    307         @Override
    308         public void hideSoftInput(int flags, ResultReceiver resultReceiver) {
    309             getTracer().hideSoftInput(flags, resultReceiver,
    310                     () -> super.hideSoftInput(flags, resultReceiver));
    311         }
    312 
    313         @Override
    314         public void attachToken(IBinder token) {
    315             getTracer().attachToken(token, () -> super.attachToken(token));
    316         }
    317 
    318         @Override
    319         public void bindInput(InputBinding binding) {
    320             getTracer().bindInput(binding, () -> super.bindInput(binding));
    321         }
    322 
    323         @Override
    324         public void unbindInput() {
    325             getTracer().unbindInput(() -> super.unbindInput());
    326         }
    327     }
    328 
    329     @Override
    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
    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
    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 
    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 
    443         private void updateBottomPaddingIfNecessary(int newPaddingBottom) {
    444             if (getPaddingBottom() != newPaddingBottom) {
    445                 setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), newPaddingBottom);
    446             }
    447         }
    448 
    449         @Override
    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
    482         protected void onDetachedFromWindow() {
    483             super.onDetachedFromWindow();
    484             removeOnLayoutChangeListener(mLayoutListener);
    485         }
    486     }
    487 
    488     private void onInputViewLayoutChanged(@NonNull ImeLayoutInfo layoutInfo) {
    489         getTracer().onInputViewLayoutChanged(layoutInfo, () -> { });
    490     }
    491 
    492     @Override
    493     public View onCreateInputView() {
    494         return getTracer().onCreateInputView(() ->
    495                 new KeyboardLayoutView(this, mSettings, this::onInputViewLayoutChanged));
    496     }
    497 
    498     @Override
    499     public void onStartInput(EditorInfo editorInfo, boolean restarting) {
    500         getTracer().onStartInput(editorInfo, restarting,
    501                 () -> super.onStartInput(editorInfo, restarting));
    502     }
    503 
    504     @Override
    505     public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
    506         getTracer().onStartInputView(editorInfo, restarting,
    507                 () -> super.onStartInputView(editorInfo, restarting));
    508     }
    509 
    510     @Override
    511     public void onFinishInputView(boolean finishingInput) {
    512         getTracer().onFinishInputView(finishingInput,
    513                 () -> super.onFinishInputView(finishingInput));
    514     }
    515 
    516     @Override
    517     public void onFinishInput() {
    518         getTracer().onFinishInput(() -> super.onFinishInput());
    519     }
    520 
    521     @Override
    522     public boolean onKeyDown(int keyCode, KeyEvent event) {
    523         return getTracer().onKeyDown(keyCode, event, () -> super.onKeyDown(keyCode, event));
    524     }
    525 
    526     @Override
    527     public void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) {
    528         getTracer().onUpdateCursorAnchorInfo(cursorAnchorInfo,
    529                 () -> super.onUpdateCursorAnchorInfo(cursorAnchorInfo));
    530     }
    531 
    532     @CallSuper
    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
    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