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