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