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