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.content.Context.MODE_PRIVATE;
20 
21 import android.app.UiAutomation;
22 import android.content.BroadcastReceiver;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.os.HandlerThread;
30 import android.os.Parcel;
31 import android.os.ParcelFileDescriptor;
32 import android.os.SystemClock;
33 import android.provider.Settings;
34 import androidx.annotation.GuardedBy;
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 import android.text.TextUtils;
38 import android.view.inputmethod.InputMethodManager;
39 
40 import com.android.compatibility.common.util.PollingCheck;
41 
42 import java.io.IOException;
43 import java.io.OutputStream;
44 import java.util.concurrent.TimeUnit;
45 
46 /**
47  * Represents an active Mock IME session, which provides basic primitives to write end-to-end tests
48  * for IME APIs.
49  *
50  * <p>To use {@link MockIme} via {@link MockImeSession}, you need to </p>
51  * <p>Public methods are not thread-safe.</p>
52  */
53 public class MockImeSession implements AutoCloseable {
54     private final String mImeEventActionName =
55             "com.android.cts.mockime.action.IME_EVENT." + SystemClock.elapsedRealtimeNanos();
56 
57     /** Setting file name to store initialization settings for {@link MockIme}. */
58     static final String MOCK_IME_SETTINGS_FILE = "mockimesettings.data";
59 
60     private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(10);
61 
62     @NonNull
63     private final Context mContext;
64     @NonNull
65     private final UiAutomation mUiAutomation;
66 
67     private final HandlerThread mHandlerThread = new HandlerThread("EventReceiver");
68 
69     private static final class EventStore {
70         private static final int INITIAL_ARRAY_SIZE = 32;
71 
72         @NonNull
73         public final ImeEvent[] mArray;
74         public int mLength;
75 
EventStore()76         EventStore() {
77             mArray = new ImeEvent[INITIAL_ARRAY_SIZE];
78             mLength = 0;
79         }
80 
EventStore(EventStore src, int newLength)81         EventStore(EventStore src, int newLength) {
82             mArray = new ImeEvent[newLength];
83             mLength = src.mLength;
84             System.arraycopy(src.mArray, 0, mArray, 0, src.mLength);
85         }
86 
add(ImeEvent event)87         public EventStore add(ImeEvent event) {
88             if (mLength + 1 <= mArray.length) {
89                 mArray[mLength] = event;
90                 ++mLength;
91                 return this;
92             } else {
93                 return new EventStore(this, mLength * 2).add(event);
94             }
95         }
96 
takeSnapshot()97         public ImeEventStream.ImeEventArray takeSnapshot() {
98             return new ImeEventStream.ImeEventArray(mArray, mLength);
99         }
100     }
101 
102     private static final class MockImeEventReceiver extends BroadcastReceiver {
103         private final Object mLock = new Object();
104 
105         @GuardedBy("mLock")
106         @NonNull
107         private EventStore mCurrentEventStore = new EventStore();
108 
109         @NonNull
110         private final String mActionName;
111 
MockImeEventReceiver(@onNull String actionName)112         MockImeEventReceiver(@NonNull String actionName) {
113             mActionName = actionName;
114         }
115 
116         @Override
onReceive(Context context, Intent intent)117         public void onReceive(Context context, Intent intent) {
118             if (TextUtils.equals(mActionName, intent.getAction())) {
119                 synchronized (mLock) {
120                     mCurrentEventStore =
121                             mCurrentEventStore.add(ImeEvent.fromBundle(intent.getExtras()));
122                 }
123             }
124         }
125 
takeEventSnapshot()126         public ImeEventStream.ImeEventArray takeEventSnapshot() {
127             synchronized (mLock) {
128                 return mCurrentEventStore.takeSnapshot();
129             }
130         }
131     }
132     private final MockImeEventReceiver mEventReceiver =
133             new MockImeEventReceiver(mImeEventActionName);
134 
135     private final ImeEventStream mEventStream =
136             new ImeEventStream(mEventReceiver::takeEventSnapshot);
137 
executeShellCommand( @onNull UiAutomation uiAutomation, @NonNull String command)138     private static String executeShellCommand(
139             @NonNull UiAutomation uiAutomation, @NonNull String command) throws IOException {
140         try (ParcelFileDescriptor.AutoCloseInputStream in =
141                      new ParcelFileDescriptor.AutoCloseInputStream(
142                              uiAutomation.executeShellCommand(command))) {
143             final StringBuilder sb = new StringBuilder();
144             final byte[] buffer = new byte[4096];
145             while (true) {
146                 final int numRead = in.read(buffer);
147                 if (numRead <= 0) {
148                     break;
149                 }
150                 sb.append(new String(buffer, 0, numRead));
151             }
152             return sb.toString();
153         }
154     }
155 
156     @Nullable
getCurrentInputMethodId()157     private String getCurrentInputMethodId() {
158         // TODO: Replace this with IMM#getCurrentInputMethodIdForTesting()
159         return Settings.Secure.getString(mContext.getContentResolver(),
160                 Settings.Secure.DEFAULT_INPUT_METHOD);
161     }
162 
163     @Nullable
writeMockImeSettings(@onNull Context context, @NonNull String imeEventActionName, @Nullable ImeSettings.Builder imeSettings)164     private static void writeMockImeSettings(@NonNull Context context,
165             @NonNull String imeEventActionName,
166             @Nullable ImeSettings.Builder imeSettings) throws Exception {
167         context.deleteFile(MOCK_IME_SETTINGS_FILE);
168         try (OutputStream os = context.openFileOutput(MOCK_IME_SETTINGS_FILE, MODE_PRIVATE)) {
169             Parcel parcel = null;
170             try {
171                 parcel = Parcel.obtain();
172                 ImeSettings.writeToParcel(parcel, imeEventActionName, imeSettings);
173                 os.write(parcel.marshall());
174             } finally {
175                 if (parcel != null) {
176                     parcel.recycle();
177                 }
178             }
179             os.flush();
180         }
181     }
182 
getMockImeComponentName()183     private ComponentName getMockImeComponentName() {
184         return MockIme.getComponentName(mContext.getPackageName());
185     }
186 
getMockImeId()187     private String getMockImeId() {
188         return MockIme.getImeId(mContext.getPackageName());
189     }
190 
MockImeSession(@onNull Context context, @NonNull UiAutomation uiAutomation)191     private MockImeSession(@NonNull Context context, @NonNull UiAutomation uiAutomation) {
192         mContext = context;
193         mUiAutomation = uiAutomation;
194     }
195 
initialize(@ullable ImeSettings.Builder imeSettings)196     private void initialize(@Nullable ImeSettings.Builder imeSettings) throws Exception {
197         // Make sure that MockIME is not selected.
198         if (mContext.getSystemService(InputMethodManager.class)
199                 .getInputMethodList()
200                 .stream()
201                 .anyMatch(info -> getMockImeComponentName().equals(info.getComponent()))) {
202             executeShellCommand(mUiAutomation, "ime reset");
203         }
204         if (mContext.getSystemService(InputMethodManager.class)
205                 .getEnabledInputMethodList()
206                 .stream()
207                 .anyMatch(info -> getMockImeComponentName().equals(info.getComponent()))) {
208             throw new IllegalStateException();
209         }
210 
211         writeMockImeSettings(mContext, mImeEventActionName, imeSettings);
212 
213         mHandlerThread.start();
214         mContext.registerReceiver(mEventReceiver,
215                 new IntentFilter(mImeEventActionName), null /* broadcastPermission */,
216                 new Handler(mHandlerThread.getLooper()));
217 
218         executeShellCommand(mUiAutomation, "ime enable " + getMockImeId());
219         executeShellCommand(mUiAutomation, "ime set " + getMockImeId());
220 
221         PollingCheck.check("Make sure that MockIME becomes available", TIMEOUT,
222                 () -> getMockImeId().equals(getCurrentInputMethodId()));
223     }
224 
225     /**
226      * Creates a new Mock IME session. During this session, you can receive various events from
227      * {@link MockIme}.
228      *
229      * @param context {@link Context} to be used to receive inter-process events from the
230      *                {@link MockIme} (e.g. via {@link BroadcastReceiver}
231      * @param uiAutomation {@link UiAutomation} object to change the device state that are typically
232      *                     guarded by permissions.
233      * @param imeSettings Key-value pairs to be passed to the {@link MockIme}.
234      * @return A session object, with which you can retrieve event logs from the {@link MockIme} and
235      *         can clean up the session.
236      */
237     @NonNull
create( @onNull Context context, @NonNull UiAutomation uiAutomation, @Nullable ImeSettings.Builder imeSettings)238     public static MockImeSession create(
239             @NonNull Context context,
240             @NonNull UiAutomation uiAutomation,
241             @Nullable ImeSettings.Builder imeSettings) throws Exception {
242         final MockImeSession client = new MockImeSession(context, uiAutomation);
243         client.initialize(imeSettings);
244         return client;
245     }
246 
247     /**
248      * @return {@link ImeEventStream} object that stores events sent from {@link MockIme} since the
249      *         session is created.
250      */
openEventStream()251     public ImeEventStream openEventStream() {
252         return mEventStream.copy();
253     }
254 
255     /**
256      * Closes the active session and de-selects {@link MockIme}. Currently which IME will be
257      * selected next is up to the system.
258      */
close()259     public void close() throws Exception {
260         executeShellCommand(mUiAutomation, "ime reset");
261 
262         PollingCheck.check("Make sure that MockIME becomes unavailable", TIMEOUT, () ->
263                 mContext.getSystemService(InputMethodManager.class)
264                         .getEnabledInputMethodList()
265                         .stream()
266                         .noneMatch(info -> getMockImeComponentName().equals(info.getComponent())));
267 
268         mContext.unregisterReceiver(mEventReceiver);
269         mHandlerThread.quitSafely();
270         mContext.deleteFile(MOCK_IME_SETTINGS_FILE);
271     }
272 
273     /**
274      * Lets {@link MockIme} to call
275      * {@link android.view.inputmethod.InputConnection#commitText(CharSequence, int)} with the given
276      * parameters.
277      *
278      * <p>This triggers {@code getCurrentInputConnection().commitText(text, newCursorPosition)}.</p>
279      *
280      * @param text to be passed as the {@code text} parameter
281      * @param newCursorPosition to be passed as the {@code newCursorPosition} parameter
282      * @return {@link ImeCommand} object that can be passed to
283      *         {@link ImeEventStreamTestUtils#expectCommand(ImeEventStream, ImeCommand, long)} to
284      *         wait until this event is handled by {@link MockIme}
285      */
286     @NonNull
callCommitText(@onNull CharSequence text, int newCursorPosition)287     public ImeCommand callCommitText(@NonNull CharSequence text, int newCursorPosition) {
288         final Bundle params = new Bundle();
289         params.putCharSequence("text", text);
290         params.putInt("newCursorPosition", newCursorPosition);
291         final ImeCommand command = new ImeCommand(
292                 "commitText", SystemClock.elapsedRealtimeNanos(), true, params);
293         final Intent intent = new Intent();
294         intent.setPackage(mContext.getPackageName());
295         intent.setAction(MockIme.getCommandActionName(mImeEventActionName));
296         intent.putExtras(command.toBundle());
297         mContext.sendBroadcast(intent);
298         return command;
299     }
300 
301     @NonNull
callSetBackDisposition(int backDisposition)302     public ImeCommand callSetBackDisposition(int backDisposition) {
303         final Bundle params = new Bundle();
304         params.putInt("backDisposition", backDisposition);
305         final ImeCommand command = new ImeCommand(
306                 "setBackDisposition", SystemClock.elapsedRealtimeNanos(), true, params);
307         final Intent intent = new Intent();
308         intent.setPackage(mContext.getPackageName());
309         intent.setAction(MockIme.getCommandActionName(mImeEventActionName));
310         intent.putExtras(command.toBundle());
311         mContext.sendBroadcast(intent);
312         return command;
313     }
314 
315     @NonNull
callRequestHideSelf(int flags)316     public ImeCommand callRequestHideSelf(int flags) {
317         final Bundle params = new Bundle();
318         params.putInt("flags", flags);
319         final ImeCommand command = new ImeCommand(
320                 "requestHideSelf", SystemClock.elapsedRealtimeNanos(), true, params);
321         final Intent intent = new Intent();
322         intent.setPackage(mContext.getPackageName());
323         intent.setAction(MockIme.getCommandActionName(mImeEventActionName));
324         intent.putExtras(command.toBundle());
325         mContext.sendBroadcast(intent);
326         return command;
327     }
328 
329     @NonNull
callRequestShowSelf(int flags)330     public ImeCommand callRequestShowSelf(int flags) {
331         final Bundle params = new Bundle();
332         params.putInt("flags", flags);
333         final ImeCommand command = new ImeCommand(
334                 "requestShowSelf", SystemClock.elapsedRealtimeNanos(), true, params);
335         final Intent intent = new Intent();
336         intent.setPackage(mContext.getPackageName());
337         intent.setAction(MockIme.getCommandActionName(mImeEventActionName));
338         intent.putExtras(command.toBundle());
339         mContext.sendBroadcast(intent);
340         return command;
341     }
342 }
343