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 android.os.SystemClock; 20 import androidx.annotation.NonNull; 21 import android.text.TextUtils; 22 import android.view.inputmethod.EditorInfo; 23 import android.view.inputmethod.InputBinding; 24 25 import java.util.Optional; 26 import java.util.concurrent.TimeoutException; 27 import java.util.function.Predicate; 28 29 /** 30 * A set of utility methods to avoid boilerplate code when writing end-to-end tests. 31 */ 32 public final class ImeEventStreamTestUtils { 33 private static final long TIME_SLICE = 50; // msec 34 35 /** 36 * Cannot be instantiated 37 */ 38 private ImeEventStreamTestUtils() {} 39 40 /** 41 * Behavior mode of {@link #expectEvent(ImeEventStream, Predicate, EventFilterMode, long)} 42 */ 43 public enum EventFilterMode { 44 /** 45 * All {@link ImeEvent} events should be checked 46 */ 47 CHECK_ALL, 48 /** 49 * Only events that return {@code true} from {@link ImeEvent#isEnterEvent()} should be 50 * checked 51 */ 52 CHECK_ENTER_EVENT_ONLY, 53 /** 54 * Only events that return {@code false} from {@link ImeEvent#isEnterEvent()} should be 55 * checked 56 */ 57 CHECK_EXIT_EVENT_ONLY, 58 } 59 60 /** 61 * Wait until an event that matches the given {@code condition} is found in the stream. 62 * 63 * <p>When this method succeeds to find an event that matches the given {@code condition}, the 64 * stream position will be set to the next to the found object then the event found is returned. 65 * </p> 66 * 67 * <p>For convenience, this method automatically filter out exit events (events that return 68 * {@code false} from {@link ImeEvent#isEnterEvent()}.</p> 69 * 70 * <p>TODO: Consider renaming this to {@code expectEventEnter} or something like that.</p> 71 * 72 * @param stream {@link ImeEventStream} to be checked. 73 * @param condition the event condition to be matched 74 * @param timeout timeout in millisecond 75 * @return {@link ImeEvent} found 76 * @throws TimeoutException when the no event is matched to the given condition within 77 * {@code timeout} 78 */ 79 @NonNull 80 public static ImeEvent expectEvent(@NonNull ImeEventStream stream, 81 @NonNull Predicate<ImeEvent> condition, long timeout) throws TimeoutException { 82 return expectEvent(stream, condition, EventFilterMode.CHECK_ENTER_EVENT_ONLY, timeout); 83 } 84 85 /** 86 * Wait until an event that matches the given {@code condition} is found in the stream. 87 * 88 * <p>When this method succeeds to find an event that matches the given {@code condition}, the 89 * stream position will be set to the next to the found object then the event found is returned. 90 * </p> 91 * 92 * @param stream {@link ImeEventStream} to be checked. 93 * @param condition the event condition to be matched 94 * @param filterMode controls how events are filtered out 95 * @param timeout timeout in millisecond 96 * @return {@link ImeEvent} found 97 * @throws TimeoutException when the no event is matched to the given condition within 98 * {@code timeout} 99 */ 100 @NonNull 101 public static ImeEvent expectEvent(@NonNull ImeEventStream stream, 102 @NonNull Predicate<ImeEvent> condition, EventFilterMode filterMode, long timeout) 103 throws TimeoutException { 104 try { 105 Optional<ImeEvent> result; 106 while (true) { 107 if (timeout < 0) { 108 throw new TimeoutException( 109 "event not found within the timeout: " + stream.dump()); 110 } 111 final Predicate<ImeEvent> combinedCondition; 112 switch (filterMode) { 113 case CHECK_ALL: 114 combinedCondition = condition; 115 break; 116 case CHECK_ENTER_EVENT_ONLY: 117 combinedCondition = event -> event.isEnterEvent() && condition.test(event); 118 break; 119 case CHECK_EXIT_EVENT_ONLY: 120 combinedCondition = event -> !event.isEnterEvent() && condition.test(event); 121 break; 122 default: 123 throw new IllegalArgumentException("Unknown filterMode " + filterMode); 124 } 125 result = stream.seekToFirst(combinedCondition); 126 if (result.isPresent()) { 127 break; 128 } 129 Thread.sleep(TIME_SLICE); 130 timeout -= TIME_SLICE; 131 } 132 final ImeEvent event = result.get(); 133 if (event == null) { 134 throw new NullPointerException("found event is null: " + stream.dump()); 135 } 136 stream.skip(1); 137 return event; 138 } catch (InterruptedException e) { 139 throw new RuntimeException("expectEvent failed: " + stream.dump(), e); 140 } 141 } 142 143 /** 144 * Checks if {@param eventName} has occurred on the EditText(or TextView) of the current 145 * activity. 146 * @param eventName event name to check 147 * @param marker Test marker set to {@link android.widget.EditText#setPrivateImeOptions(String)} 148 * @return true if event occurred. 149 */ 150 public static Predicate<ImeEvent> editorMatcher( 151 @NonNull String eventName, @NonNull String marker) { 152 return event -> { 153 if (!TextUtils.equals(eventName, event.getEventName())) { 154 return false; 155 } 156 final EditorInfo editorInfo = event.getArguments().getParcelable("editorInfo"); 157 return TextUtils.equals(marker, editorInfo.privateImeOptions); 158 }; 159 } 160 161 /** 162 * Wait until an event that matches the given command is consumed by the {@link MockIme}. 163 * 164 * <p>For convenience, this method automatically filter out enter events (events that return 165 * {@code true} from {@link ImeEvent#isEnterEvent()}.</p> 166 * 167 * <p>TODO: Consider renaming this to {@code expectCommandConsumed} or something like that.</p> 168 * 169 * @param stream {@link ImeEventStream} to be checked. 170 * @param command {@link ImeCommand} to be waited for. 171 * @param timeout timeout in millisecond 172 * @return {@link ImeEvent} found 173 * @throws TimeoutException when the no event is matched to the given condition within 174 * {@code timeout} 175 */ 176 @NonNull 177 public static ImeEvent expectCommand(@NonNull ImeEventStream stream, 178 @NonNull ImeCommand command, long timeout) throws TimeoutException { 179 final Predicate<ImeEvent> predicate = event -> { 180 if (!TextUtils.equals("onHandleCommand", event.getEventName())) { 181 return false; 182 } 183 final ImeCommand eventCommand = 184 ImeCommand.fromBundle(event.getArguments().getBundle("command")); 185 return eventCommand.getId() == command.getId(); 186 }; 187 return expectEvent(stream, predicate, EventFilterMode.CHECK_EXIT_EVENT_ONLY, timeout); 188 } 189 190 /** 191 * Assert that an event that matches the given {@code condition} will no be found in the stream 192 * within the given {@code timeout}. 193 * 194 * <p>When this method succeeds, the stream position will not change.</p> 195 * 196 * <p>For convenience, this method automatically filter out exit events (events that return 197 * {@code false} from {@link ImeEvent#isEnterEvent()}.</p> 198 * 199 * <p>TODO: Consider renaming this to {@code notExpectEventEnter} or something like that.</p> 200 * 201 * @param stream {@link ImeEventStream} to be checked. 202 * @param condition the event condition to be matched 203 * @param timeout timeout in millisecond 204 * @throws AssertionError if such an event is found within the given {@code timeout} 205 */ 206 public static void notExpectEvent(@NonNull ImeEventStream stream, 207 @NonNull Predicate<ImeEvent> condition, long timeout) { 208 notExpectEvent(stream, condition, EventFilterMode.CHECK_ENTER_EVENT_ONLY, timeout); 209 } 210 211 /** 212 * Assert that an event that matches the given {@code condition} will no be found in the stream 213 * within the given {@code timeout}. 214 * 215 * <p>When this method succeeds, the stream position will not change.</p> 216 * 217 * @param stream {@link ImeEventStream} to be checked. 218 * @param condition the event condition to be matched 219 * @param filterMode controls how events are filtered out 220 * @param timeout timeout in millisecond 221 * @throws AssertionError if such an event is found within the given {@code timeout} 222 */ 223 public static void notExpectEvent(@NonNull ImeEventStream stream, 224 @NonNull Predicate<ImeEvent> condition, EventFilterMode filterMode, long timeout) { 225 final Predicate<ImeEvent> combinedCondition; 226 switch (filterMode) { 227 case CHECK_ALL: 228 combinedCondition = condition; 229 break; 230 case CHECK_ENTER_EVENT_ONLY: 231 combinedCondition = event -> event.isEnterEvent() && condition.test(event); 232 break; 233 case CHECK_EXIT_EVENT_ONLY: 234 combinedCondition = event -> !event.isEnterEvent() && condition.test(event); 235 break; 236 default: 237 throw new IllegalArgumentException("Unknown filterMode " + filterMode); 238 } 239 try { 240 while (true) { 241 if (timeout < 0) { 242 return; 243 } 244 if (stream.findFirst(combinedCondition).isPresent()) { 245 throw new AssertionError("notExpectEvent failed: " + stream.dump()); 246 } 247 Thread.sleep(TIME_SLICE); 248 timeout -= TIME_SLICE; 249 } 250 } catch (InterruptedException e) { 251 throw new RuntimeException("notExpectEvent failed: " + stream.dump(), e); 252 } 253 } 254 255 /** 256 * A specialized version of {@link #expectEvent(ImeEventStream, Predicate, long)} to wait for 257 * {@link android.view.inputmethod.InputMethod#bindInput(InputBinding)}. 258 * 259 * @param stream {@link ImeEventStream} to be checked. 260 * @param targetProcessPid PID to be matched to {@link InputBinding#getPid()} 261 * @param timeout timeout in millisecond 262 * @throws TimeoutException when "bindInput" is not called within {@code timeout} msec 263 */ 264 public static void expectBindInput(@NonNull ImeEventStream stream, int targetProcessPid, 265 long timeout) throws TimeoutException { 266 expectEvent(stream, event -> { 267 if (!TextUtils.equals("bindInput", event.getEventName())) { 268 return false; 269 } 270 final InputBinding binding = event.getArguments().getParcelable("binding"); 271 return binding.getPid() == targetProcessPid; 272 }, EventFilterMode.CHECK_EXIT_EVENT_ONLY, timeout); 273 } 274 275 /** 276 * Waits until {@code MockIme} does not send {@code "onInputViewLayoutChanged"} event 277 * for a certain period of time ({@code stableThresholdTime} msec). 278 * 279 * <p>When this returns non-null {@link ImeLayoutInfo}, the stream position will be set to 280 * the next event of the returned layout event. Otherwise this method does not change stream 281 * position.</p> 282 * @param stream {@link ImeEventStream} to be checked. 283 * @param stableThresholdTime threshold time to consider that {@link MockIme}'s layout is 284 * stable, in millisecond 285 * @return last {@link ImeLayoutInfo} if {@link MockIme} sent one or more 286 * {@code "onInputViewLayoutChanged"} event. Otherwise {@code null} 287 */ 288 public static ImeLayoutInfo waitForInputViewLayoutStable(@NonNull ImeEventStream stream, 289 long stableThresholdTime) { 290 ImeLayoutInfo lastLayout = null; 291 final Predicate<ImeEvent> layoutFilter = event -> 292 !event.isEnterEvent() && event.getEventName().equals("onInputViewLayoutChanged"); 293 try { 294 long deadline = SystemClock.elapsedRealtime() + stableThresholdTime; 295 while (true) { 296 if (deadline < SystemClock.elapsedRealtime()) { 297 return lastLayout; 298 } 299 final Optional<ImeEvent> event = stream.seekToFirst(layoutFilter); 300 if (event.isPresent()) { 301 // Remember the last event and extend the deadline again. 302 lastLayout = ImeLayoutInfo.readFromBundle(event.get().getArguments()); 303 deadline = SystemClock.elapsedRealtime() + stableThresholdTime; 304 stream.skip(1); 305 } 306 Thread.sleep(TIME_SLICE); 307 } 308 } catch (InterruptedException e) { 309 throw new RuntimeException("notExpectEvent failed: " + stream.dump(), e); 310 } 311 } 312 } 313