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