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.ViewGroup.LayoutParams.MATCH_PARENT; 20 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 21 import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; 22 23 import android.content.BroadcastReceiver; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.content.pm.PackageManager; 29 import android.content.res.Configuration; 30 import android.graphics.Bitmap; 31 import android.inputmethodservice.InputMethodService; 32 import android.os.Build; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.HandlerThread; 36 import android.os.IBinder; 37 import android.os.Looper; 38 import android.os.Process; 39 import android.os.ResultReceiver; 40 import android.os.StrictMode; 41 import android.os.SystemClock; 42 import android.text.TextUtils; 43 import android.util.Log; 44 import android.util.Size; 45 import android.util.TypedValue; 46 import android.view.Display; 47 import android.view.GestureDetector; 48 import android.view.Gravity; 49 import android.view.KeyEvent; 50 import android.view.View; 51 import android.view.ViewConfiguration; 52 import android.view.Window; 53 import android.view.WindowInsets; 54 import android.view.WindowManager; 55 import android.view.inputmethod.CompletionInfo; 56 import android.view.inputmethod.CorrectionInfo; 57 import android.view.inputmethod.CursorAnchorInfo; 58 import android.view.inputmethod.EditorInfo; 59 import android.view.inputmethod.ExtractedTextRequest; 60 import android.view.inputmethod.InlineSuggestion; 61 import android.view.inputmethod.InlineSuggestionsRequest; 62 import android.view.inputmethod.InlineSuggestionsResponse; 63 import android.view.inputmethod.InputBinding; 64 import android.view.inputmethod.InputConnection; 65 import android.view.inputmethod.InputContentInfo; 66 import android.view.inputmethod.InputMethod; 67 import android.widget.FrameLayout; 68 import android.widget.HorizontalScrollView; 69 import android.widget.ImageView; 70 import android.widget.LinearLayout; 71 import android.widget.TextView; 72 import android.widget.inline.InlinePresentationSpec; 73 74 import androidx.annotation.AnyThread; 75 import androidx.annotation.CallSuper; 76 import androidx.annotation.MainThread; 77 import androidx.annotation.NonNull; 78 import androidx.annotation.Nullable; 79 import androidx.annotation.WorkerThread; 80 import androidx.autofill.inline.UiVersions; 81 import androidx.autofill.inline.UiVersions.StylesBuilder; 82 import androidx.autofill.inline.v1.InlineSuggestionUi; 83 84 import java.util.ArrayList; 85 import java.util.concurrent.ExecutorService; 86 import java.util.concurrent.Executors; 87 import java.util.concurrent.atomic.AtomicBoolean; 88 import java.util.concurrent.atomic.AtomicInteger; 89 import java.util.concurrent.atomic.AtomicReference; 90 import java.util.function.BooleanSupplier; 91 import java.util.function.Consumer; 92 import java.util.function.Supplier; 93 94 /** 95 * Mock IME for end-to-end tests. 96 */ 97 public final class MockIme extends InputMethodService { 98 99 private static final String TAG = "MockIme"; 100 101 private static final String PACKAGE_NAME = "com.android.cts.mockime"; 102 getComponentName()103 static ComponentName getComponentName() { 104 return new ComponentName(PACKAGE_NAME, MockIme.class.getName()); 105 } 106 getImeId()107 static String getImeId() { 108 return getComponentName().flattenToShortString(); 109 } 110 getCommandActionName(@onNull String eventActionName)111 static String getCommandActionName(@NonNull String eventActionName) { 112 return eventActionName + ".command"; 113 } 114 115 private final HandlerThread mHandlerThread = new HandlerThread("CommandReceiver"); 116 117 private final Handler mMainHandler = new Handler(); 118 119 private final Configuration mLastDispatchedConfiguration = new Configuration(); 120 121 private static final class CommandReceiver extends BroadcastReceiver { 122 @NonNull 123 private final String mActionName; 124 @NonNull 125 private final Consumer<ImeCommand> mOnReceiveCommand; 126 CommandReceiver(@onNull String actionName, @NonNull Consumer<ImeCommand> onReceiveCommand)127 CommandReceiver(@NonNull String actionName, 128 @NonNull Consumer<ImeCommand> onReceiveCommand) { 129 mActionName = actionName; 130 mOnReceiveCommand = onReceiveCommand; 131 } 132 133 @Override onReceive(Context context, Intent intent)134 public void onReceive(Context context, Intent intent) { 135 if (TextUtils.equals(mActionName, intent.getAction())) { 136 mOnReceiveCommand.accept(ImeCommand.fromBundle(intent.getExtras())); 137 } 138 } 139 } 140 141 @Nullable 142 private InputConnection mMemorizedInputConnection = null; 143 144 @Nullable 145 @MainThread getMemorizedOrCurrentInputConnection()146 private InputConnection getMemorizedOrCurrentInputConnection() { 147 return mMemorizedInputConnection != null 148 ? mMemorizedInputConnection : getCurrentInputConnection(); 149 } 150 151 @WorkerThread onReceiveCommand(@onNull ImeCommand command)152 private void onReceiveCommand(@NonNull ImeCommand command) { 153 getTracer().onReceiveCommand(command, () -> { 154 if (command.shouldDispatchToMainThread()) { 155 mMainHandler.post(() -> onHandleCommand(command)); 156 } else { 157 onHandleCommand(command); 158 } 159 }); 160 } 161 162 @AnyThread onHandleCommand(@onNull ImeCommand command)163 private void onHandleCommand(@NonNull ImeCommand command) { 164 getTracer().onHandleCommand(command, () -> { 165 if (command.shouldDispatchToMainThread()) { 166 if (Looper.myLooper() != Looper.getMainLooper()) { 167 throw new IllegalStateException("command " + command 168 + " should be handled on the main thread"); 169 } 170 // The context which created from InputMethodService#createXXXContext must behave 171 // like an UI context, which can obtain a display, a window manager, 172 // a view configuration and a gesture detector instance without strict mode 173 // violation. 174 final Configuration testConfig = new Configuration(); 175 testConfig.setToDefaults(); 176 final Context configContext = createConfigurationContext(testConfig); 177 final Context attrContext = createAttributionContext(null /* attributionTag */); 178 // UI component accesses on a display context must throw strict mode violations. 179 final Context displayContext = createDisplayContext(getDisplay()); 180 switch (command.getName()) { 181 case "memorizeCurrentInputConnection": { 182 if (!Looper.getMainLooper().isCurrentThread()) { 183 return new UnsupportedOperationException( 184 "memorizeCurrentInputConnection can be requested only for the" 185 + " main thread."); 186 } 187 mMemorizedInputConnection = getCurrentInputConnection(); 188 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 189 } 190 case "unmemorizeCurrentInputConnection": { 191 if (!Looper.getMainLooper().isCurrentThread()) { 192 return new UnsupportedOperationException( 193 "unmemorizeCurrentInputConnection can be requested only for the" 194 + " main thread."); 195 } 196 mMemorizedInputConnection = null; 197 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 198 } 199 case "getTextBeforeCursor": { 200 final int n = command.getExtras().getInt("n"); 201 final int flag = command.getExtras().getInt("flag"); 202 return getMemorizedOrCurrentInputConnection().getTextBeforeCursor(n, flag); 203 } 204 case "getTextAfterCursor": { 205 final int n = command.getExtras().getInt("n"); 206 final int flag = command.getExtras().getInt("flag"); 207 return getMemorizedOrCurrentInputConnection().getTextAfterCursor(n, flag); 208 } 209 case "getSelectedText": { 210 final int flag = command.getExtras().getInt("flag"); 211 return getMemorizedOrCurrentInputConnection().getSelectedText(flag); 212 } 213 case "getCursorCapsMode": { 214 final int reqModes = command.getExtras().getInt("reqModes"); 215 return getMemorizedOrCurrentInputConnection().getCursorCapsMode(reqModes); 216 } 217 case "getExtractedText": { 218 final ExtractedTextRequest request = 219 command.getExtras().getParcelable("request"); 220 final int flags = command.getExtras().getInt("flags"); 221 return getMemorizedOrCurrentInputConnection().getExtractedText(request, 222 flags); 223 } 224 case "deleteSurroundingText": { 225 final int beforeLength = command.getExtras().getInt("beforeLength"); 226 final int afterLength = command.getExtras().getInt("afterLength"); 227 return getMemorizedOrCurrentInputConnection().deleteSurroundingText( 228 beforeLength, afterLength); 229 } 230 case "deleteSurroundingTextInCodePoints": { 231 final int beforeLength = command.getExtras().getInt("beforeLength"); 232 final int afterLength = command.getExtras().getInt("afterLength"); 233 return getMemorizedOrCurrentInputConnection() 234 .deleteSurroundingTextInCodePoints(beforeLength, afterLength); 235 } 236 case "setComposingText": { 237 final CharSequence text = command.getExtras().getCharSequence("text"); 238 final int newCursorPosition = 239 command.getExtras().getInt("newCursorPosition"); 240 return getMemorizedOrCurrentInputConnection().setComposingText( 241 text, newCursorPosition); 242 } 243 case "setComposingRegion": { 244 final int start = command.getExtras().getInt("start"); 245 final int end = command.getExtras().getInt("end"); 246 return getMemorizedOrCurrentInputConnection().setComposingRegion(start, 247 end); 248 } 249 case "finishComposingText": 250 return getMemorizedOrCurrentInputConnection().finishComposingText(); 251 case "commitText": { 252 final CharSequence text = command.getExtras().getCharSequence("text"); 253 final int newCursorPosition = 254 command.getExtras().getInt("newCursorPosition"); 255 return getMemorizedOrCurrentInputConnection().commitText(text, 256 newCursorPosition); 257 } 258 case "commitCompletion": { 259 final CompletionInfo text = command.getExtras().getParcelable("text"); 260 return getMemorizedOrCurrentInputConnection().commitCompletion(text); 261 } 262 case "commitCorrection": { 263 final CorrectionInfo correctionInfo = 264 command.getExtras().getParcelable("correctionInfo"); 265 return getMemorizedOrCurrentInputConnection().commitCorrection( 266 correctionInfo); 267 } 268 case "setSelection": { 269 final int start = command.getExtras().getInt("start"); 270 final int end = command.getExtras().getInt("end"); 271 return getMemorizedOrCurrentInputConnection().setSelection(start, end); 272 } 273 case "performEditorAction": { 274 final int editorAction = command.getExtras().getInt("editorAction"); 275 return getMemorizedOrCurrentInputConnection().performEditorAction( 276 editorAction); 277 } 278 case "performContextMenuAction": { 279 final int id = command.getExtras().getInt("id"); 280 return getMemorizedOrCurrentInputConnection().performContextMenuAction(id); 281 } 282 case "beginBatchEdit": 283 return getMemorizedOrCurrentInputConnection().beginBatchEdit(); 284 case "endBatchEdit": 285 return getMemorizedOrCurrentInputConnection().endBatchEdit(); 286 case "sendKeyEvent": { 287 final KeyEvent event = command.getExtras().getParcelable("event"); 288 return getMemorizedOrCurrentInputConnection().sendKeyEvent(event); 289 } 290 case "clearMetaKeyStates": { 291 final int states = command.getExtras().getInt("states"); 292 return getMemorizedOrCurrentInputConnection().clearMetaKeyStates(states); 293 } 294 case "reportFullscreenMode": { 295 final boolean enabled = command.getExtras().getBoolean("enabled"); 296 return getMemorizedOrCurrentInputConnection().reportFullscreenMode(enabled); 297 } 298 case "performSpellCheck": { 299 return getMemorizedOrCurrentInputConnection().performSpellCheck(); 300 } 301 case "performPrivateCommand": { 302 final String action = command.getExtras().getString("action"); 303 final Bundle data = command.getExtras().getBundle("data"); 304 return getMemorizedOrCurrentInputConnection().performPrivateCommand(action, 305 data); 306 } 307 case "requestCursorUpdates": { 308 final int cursorUpdateMode = command.getExtras().getInt("cursorUpdateMode"); 309 return getMemorizedOrCurrentInputConnection().requestCursorUpdates( 310 cursorUpdateMode); 311 } 312 case "getHandler": 313 return getMemorizedOrCurrentInputConnection().getHandler(); 314 case "closeConnection": 315 getMemorizedOrCurrentInputConnection().closeConnection(); 316 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 317 case "commitContent": { 318 final InputContentInfo inputContentInfo = 319 command.getExtras().getParcelable("inputContentInfo"); 320 final int flags = command.getExtras().getInt("flags"); 321 final Bundle opts = command.getExtras().getBundle("opts"); 322 return getMemorizedOrCurrentInputConnection().commitContent( 323 inputContentInfo, flags, opts); 324 } 325 case "setBackDisposition": { 326 final int backDisposition = 327 command.getExtras().getInt("backDisposition"); 328 setBackDisposition(backDisposition); 329 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 330 } 331 case "requestHideSelf": { 332 final int flags = command.getExtras().getInt("flags"); 333 requestHideSelf(flags); 334 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 335 } 336 case "requestShowSelf": { 337 final int flags = command.getExtras().getInt("flags"); 338 requestShowSelf(flags); 339 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 340 } 341 case "sendDownUpKeyEvents": { 342 final int keyEventCode = command.getExtras().getInt("keyEventCode"); 343 sendDownUpKeyEvents(keyEventCode); 344 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 345 } 346 case "getApplicationInfo": { 347 final String packageName = command.getExtras().getString("packageName"); 348 final int flags = command.getExtras().getInt("flags"); 349 try { 350 return getPackageManager().getApplicationInfo(packageName, flags); 351 } catch (PackageManager.NameNotFoundException e) { 352 return e; 353 } 354 } 355 case "getDisplayId": 356 return getDisplay().getDisplayId(); 357 case "verifyLayoutInflaterContext": 358 return getLayoutInflater().getContext() == this; 359 case "setHeight": 360 final int height = command.getExtras().getInt("height"); 361 mView.setHeight(height); 362 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 363 case "setInlineSuggestionsExtras": 364 mInlineSuggestionsExtras = command.getExtras(); 365 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 366 case "verifyGetDisplay": 367 try { 368 return verifyGetDisplay(); 369 } catch (UnsupportedOperationException e) { 370 return e; 371 } 372 case "verifyIsUiContext": 373 return verifyIsUiContext(); 374 case "verifyGetWindowManager": { 375 final WindowManager imsWm = getSystemService(WindowManager.class); 376 final WindowManager configContextWm = 377 configContext.getSystemService(WindowManager.class); 378 final WindowManager attrContextWm = 379 attrContext.getSystemService(WindowManager.class); 380 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 381 } 382 case "verifyGetViewConfiguration": { 383 final ViewConfiguration imsViewConfig = ViewConfiguration.get(this); 384 final ViewConfiguration configContextViewConfig = 385 ViewConfiguration.get(configContext); 386 final ViewConfiguration attrContextViewConfig = 387 ViewConfiguration.get(attrContext); 388 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 389 } 390 case "verifyGetGestureDetector": { 391 GestureDetector.SimpleOnGestureListener listener = 392 new GestureDetector.SimpleOnGestureListener(); 393 final GestureDetector imsGestureDetector = 394 new GestureDetector(this, listener); 395 final GestureDetector configContextGestureDetector = 396 new GestureDetector(configContext, listener); 397 final GestureDetector attrGestureDetector = 398 new GestureDetector(attrContext, listener); 399 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 400 } 401 case "verifyGetWindowManagerOnDisplayContext": { 402 // Obtaining a WindowManager on a display context must throw a strict mode 403 // violation. 404 final WindowManager wm = displayContext 405 .getSystemService(WindowManager.class); 406 407 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 408 } 409 case "verifyGetViewConfigurationOnDisplayContext": { 410 // Obtaining a ViewConfiguration on a display context must throw a strict 411 // mode violation. 412 final ViewConfiguration viewConfiguration = 413 ViewConfiguration.get(displayContext); 414 415 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 416 } 417 case "verifyGetGestureDetectorOnDisplayContext": { 418 // Obtaining a GestureDetector on a display context must throw a strict mode 419 // violation. 420 GestureDetector.SimpleOnGestureListener listener = 421 new GestureDetector.SimpleOnGestureListener(); 422 final GestureDetector gestureDetector = 423 new GestureDetector(displayContext, listener); 424 425 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 426 } 427 case "getCurrentWindowMetricsBounds": { 428 return getSystemService(WindowManager.class) 429 .getCurrentWindowMetrics().getBounds(); 430 } 431 } 432 } 433 return ImeEvent.RETURN_VALUE_UNAVAILABLE; 434 }); 435 } 436 verifyGetDisplay()437 private boolean verifyGetDisplay() throws UnsupportedOperationException { 438 final Display display; 439 final Display configContextDisplay; 440 final Configuration config = new Configuration(); 441 config.setToDefaults(); 442 final Context configContext = createConfigurationContext(config); 443 display = getDisplay(); 444 configContextDisplay = configContext.getDisplay(); 445 return display != null && configContextDisplay != null; 446 } 447 verifyIsUiContext()448 private boolean verifyIsUiContext() { 449 final Configuration config = new Configuration(); 450 config.setToDefaults(); 451 final Context configContext = createConfigurationContext(config); 452 // The value must be true because ConfigurationContext is derived from InputMethodService, 453 // which is a UI Context. 454 final boolean imeDerivedConfigContext = configContext.isUiContext(); 455 // The value must be false because DisplayContext won't receive any config update from 456 // server. 457 final boolean imeDerivedDisplayContext = createDisplayContext(getDisplay()).isUiContext(); 458 return isUiContext() && imeDerivedConfigContext && !imeDerivedDisplayContext; 459 } 460 461 @Nullable 462 private Bundle mInlineSuggestionsExtras; 463 464 @Nullable 465 private CommandReceiver mCommandReceiver; 466 467 @Nullable 468 private ImeSettings mSettings; 469 470 private final AtomicReference<String> mImeEventActionName = new AtomicReference<>(); 471 472 @Nullable getImeEventActionName()473 String getImeEventActionName() { 474 return mImeEventActionName.get(); 475 } 476 477 private final AtomicReference<String> mClientPackageName = new AtomicReference<>(); 478 479 @Nullable getClientPackageName()480 String getClientPackageName() { 481 return mClientPackageName.get(); 482 } 483 484 private class MockInputMethodImpl extends InputMethodImpl { 485 @Override showSoftInput(int flags, ResultReceiver resultReceiver)486 public void showSoftInput(int flags, ResultReceiver resultReceiver) { 487 getTracer().showSoftInput(flags, resultReceiver, 488 () -> super.showSoftInput(flags, resultReceiver)); 489 } 490 491 @Override hideSoftInput(int flags, ResultReceiver resultReceiver)492 public void hideSoftInput(int flags, ResultReceiver resultReceiver) { 493 getTracer().hideSoftInput(flags, resultReceiver, 494 () -> super.hideSoftInput(flags, resultReceiver)); 495 } 496 497 @Override attachToken(IBinder token)498 public void attachToken(IBinder token) { 499 getTracer().attachToken(token, () -> super.attachToken(token)); 500 } 501 502 @Override bindInput(InputBinding binding)503 public void bindInput(InputBinding binding) { 504 getTracer().bindInput(binding, () -> super.bindInput(binding)); 505 } 506 507 @Override unbindInput()508 public void unbindInput() { 509 getTracer().unbindInput(() -> super.unbindInput()); 510 } 511 } 512 513 @Override onCreate()514 public void onCreate() { 515 // Initialize minimum settings to send events in Tracer#onCreate(). 516 mSettings = SettingsProvider.getSettings(); 517 if (mSettings == null) { 518 throw new IllegalStateException("Settings file is not found. " 519 + "Make sure MockImeSession.create() is used to launch Mock IME."); 520 } 521 mClientPackageName.set(mSettings.getClientPackageName()); 522 mImeEventActionName.set(mSettings.getEventCallbackActionName()); 523 524 // TODO(b/159593676): consider to detect more violations 525 if (mSettings.isStrictModeEnabled()) { 526 StrictMode.setVmPolicy( 527 new StrictMode.VmPolicy.Builder() 528 .detectIncorrectContextUse() 529 .penaltyLog() 530 .penaltyListener(Runnable::run, 531 v -> getTracer().onStrictModeViolated(() -> { })) 532 .build()); 533 } 534 535 getTracer().onCreate(() -> { 536 super.onCreate(); 537 mHandlerThread.start(); 538 final String actionName = getCommandActionName(mSettings.getEventCallbackActionName()); 539 mCommandReceiver = new CommandReceiver(actionName, this::onReceiveCommand); 540 final IntentFilter filter = new IntentFilter(actionName); 541 final Handler handler = new Handler(mHandlerThread.getLooper()); 542 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 543 registerReceiver(mCommandReceiver, filter, null /* broadcastPermission */, handler, 544 Context.RECEIVER_VISIBLE_TO_INSTANT_APPS); 545 } else { 546 registerReceiver(mCommandReceiver, filter, null /* broadcastPermission */, handler); 547 } 548 if (mSettings.isVerifyContextApisInOnCreate()) { 549 getTracer().onVerify("isUiContext", this::verifyIsUiContext); 550 getTracer().onVerify("getDisplay", this::verifyGetDisplay); 551 } 552 final int windowFlags = mSettings.getWindowFlags(0); 553 final int windowFlagsMask = mSettings.getWindowFlagsMask(0); 554 if (windowFlags != 0 || windowFlagsMask != 0) { 555 final int prevFlags = getWindow().getWindow().getAttributes().flags; 556 getWindow().getWindow().setFlags(windowFlags, windowFlagsMask); 557 // For some reasons, seems that we need to post another requestLayout() when 558 // FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS bit is changed. 559 // TODO: Investigate the reason. 560 if ((windowFlagsMask & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0) { 561 final boolean hadFlag = (prevFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0; 562 final boolean hasFlag = (windowFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0; 563 if (hadFlag != hasFlag) { 564 final View decorView = getWindow().getWindow().getDecorView(); 565 decorView.post(() -> decorView.requestLayout()); 566 } 567 } 568 } 569 570 // Ensuring bar contrast interferes with the tests. 571 getWindow().getWindow().setStatusBarContrastEnforced(false); 572 getWindow().getWindow().setNavigationBarContrastEnforced(false); 573 574 if (mSettings.hasNavigationBarColor()) { 575 getWindow().getWindow().setNavigationBarColor(mSettings.getNavigationBarColor()); 576 } 577 578 // Initialize to current Configuration to prevent unexpected configDiff value dispatched 579 // in IME event. 580 mLastDispatchedConfiguration.setTo(getResources().getConfiguration()); 581 }); 582 } 583 584 @Override onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly)585 public void onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly) { 586 getTracer().onConfigureWindow(win, isFullscreen, isCandidatesOnly, 587 () -> super.onConfigureWindow(win, isFullscreen, isCandidatesOnly)); 588 } 589 590 @Override onEvaluateFullscreenMode()591 public boolean onEvaluateFullscreenMode() { 592 return getTracer().onEvaluateFullscreenMode(() -> 593 mSettings.fullscreenModeAllowed(false) && super.onEvaluateFullscreenMode()); 594 } 595 596 private static final class KeyboardLayoutView extends LinearLayout { 597 @NonNull 598 private final MockIme mMockIme; 599 @NonNull 600 private final ImeSettings mSettings; 601 @NonNull 602 private final View.OnLayoutChangeListener mLayoutListener; 603 604 private final LinearLayout mLayout; 605 606 @Nullable 607 private final LinearLayout mSuggestionView; 608 609 private boolean mDrawsBehindNavBar = false; 610 KeyboardLayoutView(MockIme mockIme, @NonNull ImeSettings imeSettings, @Nullable Consumer<ImeLayoutInfo> onInputViewLayoutChangedCallback)611 KeyboardLayoutView(MockIme mockIme, @NonNull ImeSettings imeSettings, 612 @Nullable Consumer<ImeLayoutInfo> onInputViewLayoutChangedCallback) { 613 super(mockIme); 614 615 mMockIme = mockIme; 616 mSettings = imeSettings; 617 618 setOrientation(VERTICAL); 619 620 final int defaultBackgroundColor = 621 getResources().getColor(android.R.color.holo_orange_dark, null); 622 623 final int mainSpacerHeight = mSettings.getInputViewHeight(LayoutParams.WRAP_CONTENT); 624 mLayout = new LinearLayout(getContext()); 625 mLayout.setOrientation(LinearLayout.VERTICAL); 626 627 if (mSettings.getInlineSuggestionsEnabled()) { 628 final HorizontalScrollView scrollView = new HorizontalScrollView(getContext()); 629 final LayoutParams scrollViewParams = new LayoutParams(MATCH_PARENT, 100); 630 scrollView.setLayoutParams(scrollViewParams); 631 632 final LinearLayout suggestionView = new LinearLayout(getContext()); 633 suggestionView.setBackgroundColor(0xFFEEEEEE); 634 final String suggestionViewContentDesc = 635 mSettings.getInlineSuggestionViewContentDesc(null /* default */); 636 if (suggestionViewContentDesc != null) { 637 suggestionView.setContentDescription(suggestionViewContentDesc); 638 } 639 scrollView.addView(suggestionView, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); 640 mSuggestionView = suggestionView; 641 642 mLayout.addView(scrollView); 643 } else { 644 mSuggestionView = null; 645 } 646 647 { 648 final FrameLayout secondaryLayout = new FrameLayout(getContext()); 649 secondaryLayout.setForegroundGravity(Gravity.CENTER); 650 651 final TextView textView = new TextView(getContext()); 652 textView.setLayoutParams(new LayoutParams(MATCH_PARENT, WRAP_CONTENT)); 653 textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); 654 textView.setGravity(Gravity.CENTER); 655 textView.setText(getImeId()); 656 textView.setBackgroundColor( 657 mSettings.getBackgroundColor(defaultBackgroundColor)); 658 secondaryLayout.addView(textView); 659 660 if (mSettings.isWatermarkEnabled(true /* defaultValue */)) { 661 final ImageView imageView = new ImageView(getContext()); 662 final Bitmap bitmap = Watermark.create(); 663 imageView.setImageBitmap(bitmap); 664 secondaryLayout.addView(imageView, 665 new FrameLayout.LayoutParams(bitmap.getWidth(), bitmap.getHeight(), 666 Gravity.CENTER)); 667 } 668 669 mLayout.addView(secondaryLayout); 670 } 671 672 addView(mLayout, MATCH_PARENT, mainSpacerHeight); 673 674 final int systemUiVisibility = mSettings.getInputViewSystemUiVisibility(0); 675 if (systemUiVisibility != 0) { 676 setSystemUiVisibility(systemUiVisibility); 677 } 678 679 if (mSettings.getDrawsBehindNavBar()) { 680 mDrawsBehindNavBar = true; 681 mMockIme.getWindow().getWindow().setDecorFitsSystemWindows(false); 682 setSystemUiVisibility(getSystemUiVisibility() 683 | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); 684 } 685 686 mLayoutListener = (View v, int left, int top, int right, int bottom, int oldLeft, 687 int oldTop, int oldRight, int oldBottom) -> 688 onInputViewLayoutChangedCallback.accept( 689 ImeLayoutInfo.fromLayoutListenerCallback( 690 v, left, top, right, bottom, oldLeft, oldTop, oldRight, 691 oldBottom)); 692 this.addOnLayoutChangeListener(mLayoutListener); 693 } 694 setHeight(int height)695 private void setHeight(int height) { 696 mLayout.getLayoutParams().height = height; 697 mLayout.requestLayout(); 698 } 699 updateBottomPaddingIfNecessary(int newPaddingBottom)700 private void updateBottomPaddingIfNecessary(int newPaddingBottom) { 701 if (getPaddingBottom() != newPaddingBottom) { 702 setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), newPaddingBottom); 703 } 704 } 705 706 @Override onApplyWindowInsets(WindowInsets insets)707 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 708 if (insets.isConsumed() 709 || mDrawsBehindNavBar 710 || (getSystemUiVisibility() & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0) { 711 // In this case we are not interested in consuming NavBar region. 712 // Make sure that the bottom padding is empty. 713 updateBottomPaddingIfNecessary(0); 714 return insets; 715 } 716 717 // In some cases the bottom system window inset is not a navigation bar. Wear devices 718 // that have bottom chin are examples. For now, assume that it's a navigation bar if it 719 // has the same height as the root window's stable bottom inset. 720 final WindowInsets rootWindowInsets = getRootWindowInsets(); 721 if (rootWindowInsets != null && (rootWindowInsets.getStableInsetBottom() 722 != insets.getSystemWindowInsetBottom())) { 723 // This is probably not a NavBar. 724 updateBottomPaddingIfNecessary(0); 725 return insets; 726 } 727 728 final int possibleNavBarHeight = insets.getSystemWindowInsetBottom(); 729 updateBottomPaddingIfNecessary(possibleNavBarHeight); 730 return possibleNavBarHeight <= 0 731 ? insets 732 : insets.replaceSystemWindowInsets( 733 insets.getSystemWindowInsetLeft(), 734 insets.getSystemWindowInsetTop(), 735 insets.getSystemWindowInsetRight(), 736 0 /* bottom */); 737 } 738 739 @Override onWindowVisibilityChanged(int visibility)740 protected void onWindowVisibilityChanged(int visibility) { 741 mMockIme.getTracer().onWindowVisibilityChanged(() -> { 742 super.onWindowVisibilityChanged(visibility); 743 }, visibility); 744 } 745 746 @Override onDetachedFromWindow()747 protected void onDetachedFromWindow() { 748 super.onDetachedFromWindow(); 749 removeOnLayoutChangeListener(mLayoutListener); 750 } 751 752 @MainThread updateInlineSuggestions( @onNull PendingInlineSuggestions pendingInlineSuggestions)753 private void updateInlineSuggestions( 754 @NonNull PendingInlineSuggestions pendingInlineSuggestions) { 755 Log.d(TAG, "updateInlineSuggestions() called: " + pendingInlineSuggestions.mTotalCount); 756 if (mSuggestionView == null || !pendingInlineSuggestions.mValid.get()) { 757 return; 758 } 759 mSuggestionView.removeAllViews(); 760 for (int i = 0; i < pendingInlineSuggestions.mTotalCount; i++) { 761 View view = pendingInlineSuggestions.mViews[i]; 762 if (view == null) { 763 continue; 764 } 765 mSuggestionView.addView(view); 766 } 767 } 768 } 769 770 KeyboardLayoutView mView; 771 onInputViewLayoutChanged(@onNull ImeLayoutInfo layoutInfo)772 private void onInputViewLayoutChanged(@NonNull ImeLayoutInfo layoutInfo) { 773 getTracer().onInputViewLayoutChanged(layoutInfo, () -> { }); 774 } 775 776 @Override onCreateInputView()777 public View onCreateInputView() { 778 return getTracer().onCreateInputView(() -> { 779 mView = new KeyboardLayoutView(this, mSettings, this::onInputViewLayoutChanged); 780 return mView; 781 }); 782 } 783 784 @Override onStartInput(EditorInfo editorInfo, boolean restarting)785 public void onStartInput(EditorInfo editorInfo, boolean restarting) { 786 getTracer().onStartInput(editorInfo, restarting, 787 () -> super.onStartInput(editorInfo, restarting)); 788 } 789 790 @Override onStartInputView(EditorInfo editorInfo, boolean restarting)791 public void onStartInputView(EditorInfo editorInfo, boolean restarting) { 792 getTracer().onStartInputView(editorInfo, restarting, 793 () -> super.onStartInputView(editorInfo, restarting)); 794 } 795 796 @Override onFinishInputView(boolean finishingInput)797 public void onFinishInputView(boolean finishingInput) { 798 getTracer().onFinishInputView(finishingInput, 799 () -> super.onFinishInputView(finishingInput)); 800 } 801 802 @Override onFinishInput()803 public void onFinishInput() { 804 getTracer().onFinishInput(() -> super.onFinishInput()); 805 } 806 807 @Override onKeyDown(int keyCode, KeyEvent event)808 public boolean onKeyDown(int keyCode, KeyEvent event) { 809 return getTracer().onKeyDown(keyCode, event, () -> super.onKeyDown(keyCode, event)); 810 } 811 812 @Override onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo)813 public void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) { 814 getTracer().onUpdateCursorAnchorInfo(cursorAnchorInfo, 815 () -> super.onUpdateCursorAnchorInfo(cursorAnchorInfo)); 816 } 817 818 @Override onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd)819 public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, 820 int candidatesStart, int candidatesEnd) { 821 getTracer().onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 822 candidatesStart, candidatesEnd, 823 () -> super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 824 candidatesStart, candidatesEnd)); 825 } 826 827 @CallSuper onEvaluateInputViewShown()828 public boolean onEvaluateInputViewShown() { 829 return getTracer().onEvaluateInputViewShown(() -> { 830 // onShowInputRequested() is indeed @CallSuper so we always call this, even when the 831 // result is ignored. 832 final boolean originalResult = super.onEvaluateInputViewShown(); 833 if (!mSettings.getHardKeyboardConfigurationBehaviorAllowed(false)) { 834 final Configuration config = getResources().getConfiguration(); 835 if (config.keyboard != Configuration.KEYBOARD_NOKEYS 836 && config.hardKeyboardHidden != Configuration.HARDKEYBOARDHIDDEN_YES) { 837 // Override the behavior of InputMethodService#onEvaluateInputViewShown() 838 return true; 839 } 840 } 841 return originalResult; 842 }); 843 } 844 845 @Override 846 public boolean onShowInputRequested(int flags, boolean configChange) { 847 return getTracer().onShowInputRequested(flags, configChange, () -> { 848 // onShowInputRequested() is not marked with @CallSuper, but just in case. 849 final boolean originalResult = super.onShowInputRequested(flags, configChange); 850 if (!mSettings.getHardKeyboardConfigurationBehaviorAllowed(false)) { 851 if ((flags & InputMethod.SHOW_EXPLICIT) == 0 852 && getResources().getConfiguration().keyboard 853 != Configuration.KEYBOARD_NOKEYS) { 854 // Override the behavior of InputMethodService#onShowInputRequested() 855 return true; 856 } 857 } 858 return originalResult; 859 }); 860 } 861 862 @Override 863 public void onDestroy() { 864 getTracer().onDestroy(() -> { 865 super.onDestroy(); 866 unregisterReceiver(mCommandReceiver); 867 mHandlerThread.quitSafely(); 868 }); 869 } 870 871 @Override 872 public AbstractInputMethodImpl onCreateInputMethodInterface() { 873 return getTracer().onCreateInputMethodInterface(() -> new MockInputMethodImpl()); 874 } 875 876 private final ThreadLocal<Tracer> mThreadLocalTracer = new ThreadLocal<>(); 877 878 private Tracer getTracer() { 879 Tracer tracer = mThreadLocalTracer.get(); 880 if (tracer == null) { 881 tracer = new Tracer(this); 882 mThreadLocalTracer.set(tracer); 883 } 884 return tracer; 885 } 886 887 @NonNull 888 private ImeState getState() { 889 final boolean hasInputBinding = getCurrentInputBinding() != null; 890 final boolean hasFallbackInputConnection = 891 !hasInputBinding 892 || getCurrentInputConnection() == getCurrentInputBinding().getConnection(); 893 return new ImeState(hasInputBinding, hasFallbackInputConnection); 894 } 895 896 private PendingInlineSuggestions mPendingInlineSuggestions; 897 898 private static final class PendingInlineSuggestions { 899 final InlineSuggestionsResponse mResponse; 900 final int mTotalCount; 901 final View[] mViews; 902 final AtomicInteger mInflatedViewCount; 903 final AtomicBoolean mValid = new AtomicBoolean(true); 904 905 PendingInlineSuggestions(InlineSuggestionsResponse response) { 906 mResponse = response; 907 mTotalCount = response.getInlineSuggestions().size(); 908 mViews = new View[mTotalCount]; 909 mInflatedViewCount = new AtomicInteger(0); 910 } 911 } 912 913 @MainThread 914 @Override 915 public InlineSuggestionsRequest onCreateInlineSuggestionsRequest(Bundle uiExtras) { 916 StylesBuilder stylesBuilder = UiVersions.newStylesBuilder(); 917 stylesBuilder.addStyle(InlineSuggestionUi.newStyleBuilder().build()); 918 Bundle styles = stylesBuilder.build(); 919 920 if (mInlineSuggestionsExtras != null) { 921 styles.putAll(mInlineSuggestionsExtras); 922 } 923 924 return getTracer().onCreateInlineSuggestionsRequest(() -> { 925 final ArrayList<InlinePresentationSpec> presentationSpecs = new ArrayList<>(); 926 presentationSpecs.add(new InlinePresentationSpec.Builder(new Size(100, 100), 927 new Size(400, 100)).setStyle(styles).build()); 928 presentationSpecs.add(new InlinePresentationSpec.Builder(new Size(100, 100), 929 new Size(400, 100)).setStyle(styles).build()); 930 931 final InlinePresentationSpec tooltipSpec = 932 new InlinePresentationSpec.Builder(new Size(100, 100), 933 new Size(400, 100)).setStyle(styles).build(); 934 final InlineSuggestionsRequest.Builder builder = 935 new InlineSuggestionsRequest.Builder(presentationSpecs) 936 .setInlineTooltipPresentationSpec(tooltipSpec) 937 .setMaxSuggestionCount(6); 938 if (mInlineSuggestionsExtras != null) { 939 builder.setExtras(mInlineSuggestionsExtras.deepCopy()); 940 } 941 return builder.build(); 942 }); 943 } 944 945 @MainThread 946 @Override 947 public boolean onInlineSuggestionsResponse(@NonNull InlineSuggestionsResponse response) { 948 return getTracer().onInlineSuggestionsResponse(response, () -> { 949 final PendingInlineSuggestions pendingInlineSuggestions = 950 new PendingInlineSuggestions(response); 951 if (mPendingInlineSuggestions != null) { 952 mPendingInlineSuggestions.mValid.set(false); 953 } 954 mPendingInlineSuggestions = pendingInlineSuggestions; 955 if (pendingInlineSuggestions.mTotalCount == 0) { 956 if (mView != null) { 957 mView.updateInlineSuggestions(pendingInlineSuggestions); 958 } 959 return true; 960 } 961 962 final ExecutorService executorService = Executors.newCachedThreadPool(); 963 for (int i = 0; i < pendingInlineSuggestions.mTotalCount; i++) { 964 final int index = i; 965 InlineSuggestion inlineSuggestion = 966 pendingInlineSuggestions.mResponse.getInlineSuggestions().get(index); 967 inlineSuggestion.inflate( 968 this, 969 new Size(WRAP_CONTENT, WRAP_CONTENT), 970 executorService, 971 suggestionView -> { 972 Log.d(TAG, "new inline suggestion view ready"); 973 if (suggestionView != null) { 974 suggestionView.setOnClickListener((v) -> { 975 getTracer().onInlineSuggestionClickedEvent(() -> { }); 976 }); 977 suggestionView.setOnLongClickListener((v) -> { 978 getTracer().onInlineSuggestionLongClickedEvent(() -> { }); 979 return true; 980 }); 981 pendingInlineSuggestions.mViews[index] = suggestionView; 982 } 983 if (pendingInlineSuggestions.mInflatedViewCount.incrementAndGet() 984 == pendingInlineSuggestions.mTotalCount 985 && pendingInlineSuggestions.mValid.get()) { 986 Log.d(TAG, "ready to display all suggestions"); 987 mMainHandler.post(() -> 988 mView.updateInlineSuggestions(pendingInlineSuggestions)); 989 } 990 }); 991 } 992 return true; 993 }); 994 } 995 996 @Override 997 public void onConfigurationChanged(Configuration configuration) { 998 getTracer().onConfigurationChanged(() -> {}, configuration); 999 mLastDispatchedConfiguration.setTo(configuration); 1000 } 1001 1002 /** 1003 * Event tracing helper class for {@link MockIme}. 1004 */ 1005 private static final class Tracer { 1006 1007 @NonNull 1008 private final MockIme mIme; 1009 1010 private final int mThreadId = Process.myTid(); 1011 1012 @NonNull 1013 private final String mThreadName = 1014 Thread.currentThread().getName() != null ? Thread.currentThread().getName() : ""; 1015 1016 private final boolean mIsMainThread = 1017 Looper.getMainLooper().getThread() == Thread.currentThread(); 1018 1019 private int mNestLevel = 0; 1020 1021 private String mImeEventActionName; 1022 1023 private String mClientPackageName; 1024 1025 Tracer(@NonNull MockIme mockIme) { 1026 mIme = mockIme; 1027 } 1028 1029 private void sendEventInternal(@NonNull ImeEvent event) { 1030 if (mImeEventActionName == null) { 1031 mImeEventActionName = mIme.getImeEventActionName(); 1032 } 1033 if (mClientPackageName == null) { 1034 mClientPackageName = mIme.getClientPackageName(); 1035 } 1036 if (mImeEventActionName == null || mClientPackageName == null) { 1037 Log.e(TAG, "Tracer cannot be used before onCreate()"); 1038 return; 1039 } 1040 final Intent intent = new Intent() 1041 .setAction(mImeEventActionName) 1042 .setPackage(mClientPackageName) 1043 .putExtras(event.toBundle()) 1044 .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY 1045 | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS); 1046 mIme.sendBroadcast(intent); 1047 } 1048 1049 private void recordEventInternal(@NonNull String eventName, @NonNull Runnable runnable) { 1050 recordEventInternal(eventName, runnable, new Bundle()); 1051 } 1052 1053 private void recordEventInternal(@NonNull String eventName, @NonNull Runnable runnable, 1054 @NonNull Bundle arguments) { 1055 recordEventInternal(eventName, () -> { 1056 runnable.run(); return ImeEvent.RETURN_VALUE_UNAVAILABLE; 1057 }, arguments); 1058 } 1059 1060 private <T> T recordEventInternal(@NonNull String eventName, 1061 @NonNull Supplier<T> supplier) { 1062 return recordEventInternal(eventName, supplier, new Bundle()); 1063 } 1064 1065 private <T> T recordEventInternal(@NonNull String eventName, 1066 @NonNull Supplier<T> supplier, @NonNull Bundle arguments) { 1067 final ImeState enterState = mIme.getState(); 1068 final long enterTimestamp = SystemClock.elapsedRealtimeNanos(); 1069 final long enterWallTime = System.currentTimeMillis(); 1070 final int nestLevel = mNestLevel; 1071 // Send enter event 1072 sendEventInternal(new ImeEvent(eventName, nestLevel, mThreadName, 1073 mThreadId, mIsMainThread, enterTimestamp, 0, enterWallTime, 1074 0, enterState, null, arguments, 1075 ImeEvent.RETURN_VALUE_UNAVAILABLE)); 1076 ++mNestLevel; 1077 T result; 1078 try { 1079 result = supplier.get(); 1080 } finally { 1081 --mNestLevel; 1082 } 1083 final long exitTimestamp = SystemClock.elapsedRealtimeNanos(); 1084 final long exitWallTime = System.currentTimeMillis(); 1085 final ImeState exitState = mIme.getState(); 1086 // Send exit event 1087 sendEventInternal(new ImeEvent(eventName, nestLevel, mThreadName, 1088 mThreadId, mIsMainThread, enterTimestamp, exitTimestamp, enterWallTime, 1089 exitWallTime, enterState, exitState, arguments, result)); 1090 return result; 1091 } 1092 1093 void onCreate(@NonNull Runnable runnable) { 1094 recordEventInternal("onCreate", runnable); 1095 } 1096 1097 void onVerify(String name, @NonNull BooleanSupplier supplier) { 1098 final Bundle arguments = new Bundle(); 1099 arguments.putString("name", name); 1100 recordEventInternal("onVerify", supplier::getAsBoolean, arguments); 1101 } 1102 1103 void onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly, 1104 @NonNull Runnable runnable) { 1105 final Bundle arguments = new Bundle(); 1106 arguments.putBoolean("isFullscreen", isFullscreen); 1107 arguments.putBoolean("isCandidatesOnly", isCandidatesOnly); 1108 recordEventInternal("onConfigureWindow", runnable, arguments); 1109 } 1110 1111 boolean onEvaluateFullscreenMode(@NonNull BooleanSupplier supplier) { 1112 return recordEventInternal("onEvaluateFullscreenMode", supplier::getAsBoolean); 1113 } 1114 1115 boolean onEvaluateInputViewShown(@NonNull BooleanSupplier supplier) { 1116 return recordEventInternal("onEvaluateInputViewShown", supplier::getAsBoolean); 1117 } 1118 1119 View onCreateInputView(@NonNull Supplier<View> supplier) { 1120 return recordEventInternal("onCreateInputView", supplier); 1121 } 1122 1123 void onStartInput(EditorInfo editorInfo, boolean restarting, @NonNull Runnable runnable) { 1124 final Bundle arguments = new Bundle(); 1125 arguments.putParcelable("editorInfo", editorInfo); 1126 arguments.putBoolean("restarting", restarting); 1127 recordEventInternal("onStartInput", runnable, arguments); 1128 } 1129 1130 void onWindowVisibilityChanged(@NonNull Runnable runnable, int visibility) { 1131 final Bundle arguments = new Bundle(); 1132 arguments.putInt("visible", visibility); 1133 recordEventInternal("onWindowVisibilityChanged", runnable, arguments); 1134 } 1135 1136 void onStartInputView(EditorInfo editorInfo, boolean restarting, 1137 @NonNull Runnable runnable) { 1138 final Bundle arguments = new Bundle(); 1139 arguments.putParcelable("editorInfo", editorInfo); 1140 arguments.putBoolean("restarting", restarting); 1141 recordEventInternal("onStartInputView", runnable, arguments); 1142 } 1143 1144 void onFinishInputView(boolean finishingInput, @NonNull Runnable runnable) { 1145 final Bundle arguments = new Bundle(); 1146 arguments.putBoolean("finishingInput", finishingInput); 1147 recordEventInternal("onFinishInputView", runnable, arguments); 1148 } 1149 1150 void onFinishInput(@NonNull Runnable runnable) { 1151 recordEventInternal("onFinishInput", runnable); 1152 } 1153 1154 boolean onKeyDown(int keyCode, KeyEvent event, @NonNull BooleanSupplier supplier) { 1155 final Bundle arguments = new Bundle(); 1156 arguments.putInt("keyCode", keyCode); 1157 arguments.putParcelable("event", event); 1158 return recordEventInternal("onKeyDown", supplier::getAsBoolean, arguments); 1159 } 1160 1161 void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo, 1162 @NonNull Runnable runnable) { 1163 final Bundle arguments = new Bundle(); 1164 arguments.putParcelable("cursorAnchorInfo", cursorAnchorInfo); 1165 recordEventInternal("onUpdateCursorAnchorInfo", runnable, arguments); 1166 } 1167 1168 void onUpdateSelection(int oldSelStart, 1169 int oldSelEnd, 1170 int newSelStart, 1171 int newSelEnd, 1172 int candidatesStart, 1173 int candidatesEnd, 1174 @NonNull Runnable runnable) { 1175 final Bundle arguments = new Bundle(); 1176 arguments.putInt("oldSelStart", oldSelStart); 1177 arguments.putInt("oldSelEnd", oldSelEnd); 1178 arguments.putInt("newSelStart", newSelStart); 1179 arguments.putInt("newSelEnd", newSelEnd); 1180 arguments.putInt("candidatesStart", candidatesStart); 1181 arguments.putInt("candidatesEnd", candidatesEnd); 1182 recordEventInternal("onUpdateSelection", runnable, arguments); 1183 } 1184 1185 boolean onShowInputRequested(int flags, boolean configChange, 1186 @NonNull BooleanSupplier supplier) { 1187 final Bundle arguments = new Bundle(); 1188 arguments.putInt("flags", flags); 1189 arguments.putBoolean("configChange", configChange); 1190 return recordEventInternal("onShowInputRequested", supplier::getAsBoolean, arguments); 1191 } 1192 1193 void onDestroy(@NonNull Runnable runnable) { 1194 recordEventInternal("onDestroy", runnable); 1195 } 1196 1197 void attachToken(IBinder token, @NonNull Runnable runnable) { 1198 final Bundle arguments = new Bundle(); 1199 arguments.putBinder("token", token); 1200 recordEventInternal("attachToken", runnable, arguments); 1201 } 1202 1203 void bindInput(InputBinding binding, @NonNull Runnable runnable) { 1204 final Bundle arguments = new Bundle(); 1205 arguments.putParcelable("binding", binding); 1206 recordEventInternal("bindInput", runnable, arguments); 1207 } 1208 1209 void unbindInput(@NonNull Runnable runnable) { 1210 recordEventInternal("unbindInput", runnable); 1211 } 1212 1213 void showSoftInput(int flags, ResultReceiver resultReceiver, @NonNull Runnable runnable) { 1214 final Bundle arguments = new Bundle(); 1215 arguments.putInt("flags", flags); 1216 arguments.putParcelable("resultReceiver", resultReceiver); 1217 recordEventInternal("showSoftInput", runnable, arguments); 1218 } 1219 1220 void hideSoftInput(int flags, ResultReceiver resultReceiver, @NonNull Runnable runnable) { 1221 final Bundle arguments = new Bundle(); 1222 arguments.putInt("flags", flags); 1223 arguments.putParcelable("resultReceiver", resultReceiver); 1224 recordEventInternal("hideSoftInput", runnable, arguments); 1225 } 1226 1227 AbstractInputMethodImpl onCreateInputMethodInterface( 1228 @NonNull Supplier<AbstractInputMethodImpl> supplier) { 1229 return recordEventInternal("onCreateInputMethodInterface", supplier); 1230 } 1231 1232 void onReceiveCommand(@NonNull ImeCommand command, @NonNull Runnable runnable) { 1233 final Bundle arguments = new Bundle(); 1234 arguments.putBundle("command", command.toBundle()); 1235 recordEventInternal("onReceiveCommand", runnable, arguments); 1236 } 1237 1238 void onHandleCommand( 1239 @NonNull ImeCommand command, @NonNull Supplier<Object> resultSupplier) { 1240 final Bundle arguments = new Bundle(); 1241 arguments.putBundle("command", command.toBundle()); 1242 recordEventInternal("onHandleCommand", resultSupplier, arguments); 1243 } 1244 1245 void onInputViewLayoutChanged(@NonNull ImeLayoutInfo imeLayoutInfo, 1246 @NonNull Runnable runnable) { 1247 final Bundle arguments = new Bundle(); 1248 imeLayoutInfo.writeToBundle(arguments); 1249 recordEventInternal("onInputViewLayoutChanged", runnable, arguments); 1250 } 1251 1252 void onStrictModeViolated(@NonNull Runnable runnable) { 1253 final Bundle arguments = new Bundle(); 1254 recordEventInternal("onStrictModeViolated", runnable, arguments); 1255 } 1256 1257 InlineSuggestionsRequest onCreateInlineSuggestionsRequest( 1258 @NonNull Supplier<InlineSuggestionsRequest> supplier) { 1259 return recordEventInternal("onCreateInlineSuggestionsRequest", supplier); 1260 } 1261 1262 boolean onInlineSuggestionsResponse(@NonNull InlineSuggestionsResponse response, 1263 @NonNull BooleanSupplier supplier) { 1264 final Bundle arguments = new Bundle(); 1265 arguments.putParcelable("response", response); 1266 return recordEventInternal("onInlineSuggestionsResponse", supplier::getAsBoolean, 1267 arguments); 1268 } 1269 1270 void onInlineSuggestionClickedEvent(@NonNull Runnable runnable) { 1271 final Bundle arguments = new Bundle(); 1272 recordEventInternal("onInlineSuggestionClickedEvent", runnable, arguments); 1273 } 1274 1275 void onInlineSuggestionLongClickedEvent(@NonNull Runnable runnable) { 1276 final Bundle arguments = new Bundle(); 1277 recordEventInternal("onInlineSuggestionLongClickedEvent", runnable, arguments); 1278 } 1279 1280 void onConfigurationChanged(@NonNull Runnable runnable, Configuration configuration) { 1281 final Bundle arguments = new Bundle(); 1282 arguments.putParcelable("Configuration", configuration); 1283 arguments.putInt("ConfigUpdates", configuration.diff( 1284 mIme.mLastDispatchedConfiguration)); 1285 recordEventInternal("onConfigurationChanged", runnable, arguments); 1286 } 1287 } 1288 } 1289