1 /* 2 * Copyright (C) 2022 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.mocka11yime; 18 19 import android.accessibilityservice.AccessibilityService; 20 import android.accessibilityservice.InputMethod; 21 import android.content.BroadcastReceiver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.os.Bundle; 26 import android.os.Handler; 27 import android.os.HandlerThread; 28 import android.os.Looper; 29 import android.os.Process; 30 import android.os.SystemClock; 31 import android.text.TextUtils; 32 import android.util.Log; 33 import android.view.KeyEvent; 34 import android.view.accessibility.AccessibilityEvent; 35 import android.view.inputmethod.EditorInfo; 36 import android.view.inputmethod.TextAttribute; 37 38 import androidx.annotation.AnyThread; 39 import androidx.annotation.MainThread; 40 import androidx.annotation.NonNull; 41 import androidx.annotation.Nullable; 42 43 import java.util.function.Consumer; 44 import java.util.function.Supplier; 45 46 /** 47 * Mock {@link AccessibilityService} for end-to-end tests of {@link InputMethod}. 48 * 49 * @implNote {@link InputMethod} is available only when a special flag 50 * {@code "flagInputMethodEditor"} is set to {@code "android:accessibilityFlags"} in 51 * AndroidManifest.xml. 52 */ 53 public final class MockA11yIme extends AccessibilityService { 54 private static final String TAG = "MockA11yIme"; 55 56 private final HandlerThread mHandlerThread = new HandlerThread("CommandReceiver"); 57 58 @Nullable 59 volatile String mEventActionName; 60 61 @Nullable 62 volatile String mClientPackageName; 63 64 @Nullable 65 volatile MockA11yImeSettings mSettings; 66 volatile boolean mDestroying = false; 67 68 private static final class CommandReceiver extends BroadcastReceiver { 69 @NonNull 70 private final String mActionName; 71 @NonNull 72 private final Consumer<MockA11yImeCommand> mOnReceiveCommand; 73 CommandReceiver(@onNull String actionName, @NonNull Consumer<MockA11yImeCommand> onReceiveCommand)74 CommandReceiver(@NonNull String actionName, 75 @NonNull Consumer<MockA11yImeCommand> onReceiveCommand) { 76 mActionName = actionName; 77 mOnReceiveCommand = onReceiveCommand; 78 } 79 80 @Override onReceive(Context context, Intent intent)81 public void onReceive(Context context, Intent intent) { 82 if (TextUtils.equals(mActionName, intent.getAction())) { 83 mOnReceiveCommand.accept(MockA11yImeCommand.fromBundle(intent.getExtras())); 84 } 85 } 86 } 87 88 @Nullable 89 private CommandReceiver mCommandReceiver; 90 91 private final ThreadLocal<Tracer> mThreadLocalTracer = new ThreadLocal<>(); 92 getTracer()93 private Tracer getTracer() { 94 Tracer tracer = mThreadLocalTracer.get(); 95 if (tracer == null) { 96 tracer = new Tracer(this); 97 mThreadLocalTracer.set(tracer); 98 } 99 return tracer; 100 } 101 102 @Override onCreate()103 public void onCreate() { 104 mSettings = MockA11yImeContentProvider.getSettings(); 105 if (mSettings == null) { 106 throw new IllegalStateException("settings can never be null here. " 107 + "Make sure A11yMockImeSession.create() is used to launch MockA11yIme."); 108 } 109 110 final String actionName = MockA11yImeContentProvider.getEventCallbackActionName(); 111 if (actionName == null) { 112 throw new IllegalStateException("actionName can never be null here. " 113 + "Make sure A11yMockImeSession.create() is used to launch MockA11yIme."); 114 } 115 mEventActionName = actionName; 116 117 mClientPackageName = MockA11yImeContentProvider.getClientPackageName(); 118 if (mClientPackageName == null) { 119 throw new IllegalStateException("clientPackageName can never be null here. " 120 + "Make sure A11yMockImeSession.create() is used to launch MockA11yIme."); 121 } 122 123 getTracer().onCreate(() -> { 124 super.onCreate(); 125 final Handler handler = Handler.createAsync(getMainLooper()); 126 mHandlerThread.start(); 127 mCommandReceiver = new CommandReceiver(actionName, command -> { 128 if (command.shouldDispatchToMainThread()) { 129 handler.post(() -> onHandleCommand(command)); 130 } else { 131 onHandleCommand(command); 132 } 133 }); 134 final IntentFilter filter = new IntentFilter(actionName); 135 registerReceiver(mCommandReceiver, filter, null /* broadcastPermission */, 136 new Handler(mHandlerThread.getLooper()), 137 Context.RECEIVER_VISIBLE_TO_INSTANT_APPS | Context.RECEIVER_EXPORTED); 138 }); 139 } 140 141 @Override onServiceConnected()142 protected void onServiceConnected() { 143 getTracer().onServiceConnected(() -> {}); 144 } 145 146 @Override onAccessibilityEvent(AccessibilityEvent event)147 public void onAccessibilityEvent(AccessibilityEvent event) { 148 getTracer().onAccessibilityEvent(event, () -> {}); 149 } 150 151 @Override onInterrupt()152 public void onInterrupt() { 153 getTracer().onInterrupt(() -> {}); 154 } 155 156 private final class InputMethodImpl extends InputMethod { InputMethodImpl(AccessibilityService service)157 InputMethodImpl(AccessibilityService service) { 158 super(service); 159 } 160 161 @Override onStartInput(EditorInfo editorInfo, boolean restarting)162 public void onStartInput(EditorInfo editorInfo, boolean restarting) { 163 getTracer().onStartInput(editorInfo, restarting, 164 () -> super.onStartInput(editorInfo, restarting)); 165 } 166 167 @Override onFinishInput()168 public void onFinishInput() { 169 getTracer().onFinishInput(super::onFinishInput); 170 } 171 172 @Override onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd)173 public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, 174 int newSelEnd, int candidatesStart, int candidatesEnd) { 175 getTracer().onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 176 candidatesStart, candidatesEnd, 177 () -> super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, 178 candidatesStart, candidatesEnd)); 179 } 180 } 181 182 @Override onDestroy()183 public void onDestroy() { 184 getTracer().onDestroy(() -> { 185 mDestroying = true; 186 super.onDestroy(); 187 unregisterReceiver(mCommandReceiver); 188 mHandlerThread.quitSafely(); 189 }); 190 } 191 192 @Override onCreateInputMethod()193 public InputMethod onCreateInputMethod() { 194 return getTracer().onCreateInputMethod(() -> new InputMethodImpl(this)); 195 } 196 197 @AnyThread onHandleCommand(@onNull MockA11yImeCommand command)198 private void onHandleCommand(@NonNull MockA11yImeCommand command) { 199 getTracer().onHandleCommand(command, () -> { 200 if (command.shouldDispatchToMainThread()) { 201 if (Looper.myLooper() != Looper.getMainLooper()) { 202 throw new IllegalStateException("command " + command 203 + " should be handled on the main thread"); 204 } 205 switch (command.getName()) { 206 case "memorizeCurrentInputConnection": { 207 if (!Looper.getMainLooper().isCurrentThread()) { 208 return new UnsupportedOperationException( 209 "memorizeCurrentInputConnection can be requested only for the" 210 + " main thread."); 211 } 212 mMemorizedInputConnection = getInputMethod().getCurrentInputConnection(); 213 return MockA11yImeEvent.RETURN_VALUE_UNAVAILABLE; 214 } 215 case "unmemorizeCurrentInputConnection": { 216 if (!Looper.getMainLooper().isCurrentThread()) { 217 return new UnsupportedOperationException( 218 "unmemorizeCurrentInputConnection can be requested only for the" 219 + " main thread."); 220 } 221 mMemorizedInputConnection = null; 222 return MockA11yImeEvent.RETURN_VALUE_UNAVAILABLE; 223 } 224 225 case "getCurrentInputStarted": { 226 if (!Looper.getMainLooper().isCurrentThread()) { 227 return new UnsupportedOperationException( 228 "getCurrentInputStarted() can be requested only for the main" 229 + " thread."); 230 } 231 return getInputMethod().getCurrentInputStarted(); 232 } 233 case "getCurrentInputEditorInfo": { 234 if (!Looper.getMainLooper().isCurrentThread()) { 235 return new UnsupportedOperationException( 236 "getCurrentInputEditorInfo() can be requested only for the main" 237 + " thread."); 238 } 239 return getInputMethod().getCurrentInputEditorInfo(); 240 } 241 case "getCurrentInputConnection": { 242 if (!Looper.getMainLooper().isCurrentThread()) { 243 return new UnsupportedOperationException( 244 "getCurrentInputConnection() can be requested only for the main" 245 + " thread."); 246 } 247 return getInputMethod().getCurrentInputConnection(); 248 } 249 250 case "commitText": { 251 final CharSequence text = command.getExtras().getCharSequence("text"); 252 final int newCursorPosition = 253 command.getExtras().getInt("newCursorPosition"); 254 final TextAttribute textAttribute = command.getExtras().getParcelable( 255 "textAttribute", TextAttribute.class); 256 getMemorizedOrCurrentInputConnection().commitText( 257 text, newCursorPosition, textAttribute); 258 return MockA11yImeEvent.RETURN_VALUE_UNAVAILABLE; 259 } 260 case "setSelection": { 261 final int start = command.getExtras().getInt("start"); 262 final int end = command.getExtras().getInt("end"); 263 getMemorizedOrCurrentInputConnection().setSelection(start, end); 264 return MockA11yImeEvent.RETURN_VALUE_UNAVAILABLE; 265 } 266 case "getSurroundingText": { 267 final int beforeLength = command.getExtras().getInt("beforeLength"); 268 final int afterLength = command.getExtras().getInt("afterLength"); 269 final int flags = command.getExtras().getInt("flags"); 270 return getMemorizedOrCurrentInputConnection().getSurroundingText( 271 beforeLength, afterLength, flags); 272 } 273 case "deleteSurroundingText": { 274 final int beforeLength = command.getExtras().getInt("beforeLength"); 275 final int afterLength = command.getExtras().getInt("afterLength"); 276 getMemorizedOrCurrentInputConnection().deleteSurroundingText( 277 beforeLength, afterLength); 278 return MockA11yImeEvent.RETURN_VALUE_UNAVAILABLE; 279 } 280 case "sendKeyEvent": { 281 final KeyEvent event = command.getExtras().getParcelable( 282 "event", KeyEvent.class); 283 getMemorizedOrCurrentInputConnection().sendKeyEvent(event); 284 return MockA11yImeEvent.RETURN_VALUE_UNAVAILABLE; 285 } 286 case "performEditorAction": { 287 final int editorAction = command.getExtras().getInt("editorAction"); 288 getMemorizedOrCurrentInputConnection().performEditorAction( 289 editorAction); 290 return MockA11yImeEvent.RETURN_VALUE_UNAVAILABLE; 291 } 292 case "performContextMenuAction": { 293 final int id = command.getExtras().getInt("id"); 294 getMemorizedOrCurrentInputConnection().performContextMenuAction(id); 295 return MockA11yImeEvent.RETURN_VALUE_UNAVAILABLE; 296 } 297 case "getCursorCapsMode": { 298 final int reqModes = command.getExtras().getInt("reqModes"); 299 return getMemorizedOrCurrentInputConnection().getCursorCapsMode(reqModes); 300 } 301 case "clearMetaKeyStates": { 302 final int states = command.getExtras().getInt("states"); 303 getMemorizedOrCurrentInputConnection().clearMetaKeyStates(states); 304 return MockA11yImeEvent.RETURN_VALUE_UNAVAILABLE; 305 } 306 307 default: 308 return MockA11yImeEvent.RETURN_VALUE_UNAVAILABLE; 309 } 310 } 311 return MockA11yImeEvent.RETURN_VALUE_UNAVAILABLE; 312 }); 313 } 314 315 @Nullable 316 private InputMethod.AccessibilityInputConnection mMemorizedInputConnection = null; 317 318 @Nullable 319 @MainThread getMemorizedOrCurrentInputConnection()320 private InputMethod.AccessibilityInputConnection getMemorizedOrCurrentInputConnection() { 321 return mMemorizedInputConnection != null 322 ? mMemorizedInputConnection : getInputMethod().getCurrentInputConnection(); 323 } 324 325 /** 326 * Event tracing helper class for {@link MockA11yIme}. 327 */ 328 private static final class Tracer { 329 @NonNull 330 private final MockA11yIme mMockA11yIme; 331 332 private final int mThreadId = Process.myTid(); 333 334 @NonNull 335 private final String mThreadName = 336 Thread.currentThread().getName() != null ? Thread.currentThread().getName() : ""; 337 338 private final boolean mIsMainThread = 339 Looper.getMainLooper().getThread() == Thread.currentThread(); 340 341 private int mNestLevel = 0; 342 343 private String mImeEventActionName; 344 345 private String mClientPackageName; 346 Tracer(@onNull MockA11yIme mockA11yIme)347 Tracer(@NonNull MockA11yIme mockA11yIme) { 348 mMockA11yIme = mockA11yIme; 349 } 350 sendEventInternal(@onNull MockA11yImeEvent event)351 private void sendEventInternal(@NonNull MockA11yImeEvent event) { 352 if (mImeEventActionName == null) { 353 mImeEventActionName = mMockA11yIme.mEventActionName; 354 } 355 if (mClientPackageName == null) { 356 mClientPackageName = mMockA11yIme.mClientPackageName; 357 } 358 if (mImeEventActionName == null || mClientPackageName == null) { 359 Log.e(TAG, "Tracer cannot be used before onCreate()"); 360 return; 361 } 362 final Intent intent = new Intent() 363 .setAction(mImeEventActionName) 364 .setPackage(mClientPackageName) 365 .putExtras(event.toBundle()) 366 .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY 367 | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS); 368 mMockA11yIme.sendBroadcast(intent); 369 } 370 recordEventInternal(@onNull String eventName, @NonNull Runnable runnable)371 private void recordEventInternal(@NonNull String eventName, @NonNull Runnable runnable) { 372 recordEventInternal(eventName, runnable, new Bundle()); 373 } 374 recordEventInternal(@onNull String eventName, @NonNull Runnable runnable, @NonNull Bundle arguments)375 private void recordEventInternal(@NonNull String eventName, @NonNull Runnable runnable, 376 @NonNull Bundle arguments) { 377 recordEventInternal(eventName, () -> { 378 runnable.run(); return MockA11yImeEvent.RETURN_VALUE_UNAVAILABLE; 379 }, arguments); 380 } 381 recordEventInternal(@onNull String eventName, @NonNull Supplier<T> supplier)382 private <T> T recordEventInternal(@NonNull String eventName, 383 @NonNull Supplier<T> supplier) { 384 return recordEventInternal(eventName, supplier, new Bundle()); 385 } 386 recordEventInternal(@onNull String eventName, @NonNull Supplier<T> supplier, @NonNull Bundle arguments)387 private <T> T recordEventInternal(@NonNull String eventName, 388 @NonNull Supplier<T> supplier, @NonNull Bundle arguments) { 389 { 390 final StringBuilder sb = new StringBuilder(); 391 sb.append(eventName).append(": "); 392 MockA11yImeBundleUtils.dumpBundle(sb, arguments); 393 Log.d(TAG, sb.toString()); 394 } 395 final long enterTimestamp = SystemClock.elapsedRealtimeNanos(); 396 final long enterWallTime = System.currentTimeMillis(); 397 final int nestLevel = mNestLevel; 398 // Send enter event 399 sendEventInternal(new MockA11yImeEvent(eventName, nestLevel, mThreadName, 400 mThreadId, mIsMainThread, enterTimestamp, 0, enterWallTime, 401 0, true /* isEnter */, arguments, 402 MockA11yImeEvent.RETURN_VALUE_UNAVAILABLE)); 403 ++mNestLevel; 404 T result; 405 try { 406 result = supplier.get(); 407 } finally { 408 --mNestLevel; 409 } 410 final long exitTimestamp = SystemClock.elapsedRealtimeNanos(); 411 final long exitWallTime = System.currentTimeMillis(); 412 // Send exit event 413 sendEventInternal(new MockA11yImeEvent(eventName, nestLevel, mThreadName, 414 mThreadId, mIsMainThread, enterTimestamp, exitTimestamp, enterWallTime, 415 exitWallTime, false /* isEnter */, arguments, result)); 416 return result; 417 } 418 onHandleCommand( @onNull MockA11yImeCommand command, @NonNull Supplier<Object> resultSupplier)419 void onHandleCommand( 420 @NonNull MockA11yImeCommand command, @NonNull Supplier<Object> resultSupplier) { 421 final Bundle arguments = new Bundle(); 422 arguments.putBundle("command", command.toBundle()); 423 recordEventInternal("onHandleCommand", resultSupplier, arguments); 424 } 425 onCreate(@onNull Runnable runnable)426 void onCreate(@NonNull Runnable runnable) { 427 recordEventInternal("onCreate", runnable); 428 } 429 onServiceConnected(@onNull Runnable runnable)430 void onServiceConnected(@NonNull Runnable runnable) { 431 recordEventInternal("onServiceCreated", runnable); 432 } 433 onAccessibilityEvent(@onNull AccessibilityEvent accessibilityEvent, @NonNull Runnable runnable)434 void onAccessibilityEvent(@NonNull AccessibilityEvent accessibilityEvent, 435 @NonNull Runnable runnable) { 436 final Bundle arguments = new Bundle(); 437 arguments.putParcelable("accessibilityEvent", accessibilityEvent); 438 recordEventInternal("onAccessibilityEvent", runnable, arguments); 439 } 440 onInterrupt(@onNull Runnable runnable)441 void onInterrupt(@NonNull Runnable runnable) { 442 recordEventInternal("onInterrupt", runnable); 443 } 444 onDestroy(@onNull Runnable runnable)445 void onDestroy(@NonNull Runnable runnable) { 446 recordEventInternal("onDestroy", runnable); 447 } 448 onStartInput(EditorInfo editorInfo, boolean restarting, @NonNull Runnable runnable)449 void onStartInput(EditorInfo editorInfo, boolean restarting, @NonNull Runnable runnable) { 450 final Bundle arguments = new Bundle(); 451 arguments.putParcelable("editorInfo", editorInfo); 452 arguments.putBoolean("restarting", restarting); 453 recordEventInternal("onStartInput", runnable, arguments); 454 } 455 onFinishInput(@onNull Runnable runnable)456 void onFinishInput(@NonNull Runnable runnable) { 457 recordEventInternal("onFinishInput", runnable); 458 } 459 onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd, @NonNull Runnable runnable)460 void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, 461 int candidatesStart, int candidatesEnd, @NonNull Runnable runnable) { 462 final Bundle arguments = new Bundle(); 463 arguments.putInt("oldSelStart", oldSelStart); 464 arguments.putInt("oldSelEnd", oldSelEnd); 465 arguments.putInt("newSelStart", newSelStart); 466 arguments.putInt("newSelEnd", newSelEnd); 467 arguments.putInt("candidatesStart", candidatesStart); 468 arguments.putInt("candidatesEnd", candidatesEnd); 469 recordEventInternal("onUpdateSelection", runnable, arguments); 470 } 471 onCreateInputMethod(@onNull Supplier<InputMethod> supplier)472 InputMethod onCreateInputMethod(@NonNull Supplier<InputMethod> supplier) { 473 final Bundle arguments = new Bundle(); 474 return recordEventInternal("onCreateInputMethod", supplier, arguments); 475 } 476 } 477 } 478