Home | History | Annotate | Download | only in mockime
      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