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.Bundle;
     20 import android.view.inputmethod.EditorInfo;
     21 
     22 import androidx.annotation.IntRange;
     23 import androidx.annotation.NonNull;
     24 
     25 import java.time.Instant;
     26 import java.time.ZoneId;
     27 import java.time.format.DateTimeFormatter;
     28 import java.util.Arrays;
     29 import java.util.Optional;
     30 import java.util.function.Predicate;
     31 import java.util.function.Supplier;
     32 
     33 /**
     34  * A utility class that provides basic query operations and wait primitives for a series of
     35  * {@link ImeEvent} sent from the {@link MockIme}.
     36  *
     37  * <p>All public methods are not thread-safe.</p>
     38  */
     39 public final class ImeEventStream {
     40 
     41     private static final String LONG_LONG_SPACES = "                                        ";
     42 
     43     private static DateTimeFormatter sSimpleDateTimeFormatter =
     44             DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS").withZone(ZoneId.systemDefault());
     45 
     46     @NonNull
     47     private final Supplier<ImeEventArray> mEventSupplier;
     48     private int mCurrentPosition;
     49 
     50     ImeEventStream(@NonNull Supplier<ImeEventArray> supplier) {
     51         this(supplier, 0 /* position */);
     52     }
     53 
     54     private ImeEventStream(@NonNull Supplier<ImeEventArray> supplier, int position) {
     55         mEventSupplier = supplier;
     56         mCurrentPosition = position;
     57     }
     58 
     59     /**
     60      * Create a copy that starts from the same event position of this stream. Once a copy is created
     61      * further event position change on this stream will not affect the copy.
     62      *
     63      * @return A new copy of this stream
     64      */
     65     public ImeEventStream copy() {
     66         return new ImeEventStream(mEventSupplier, mCurrentPosition);
     67     }
     68 
     69     /**
     70      * Advances the current event position by skipping events.
     71      *
     72      * @param length number of events to be skipped
     73      * @throws IllegalArgumentException {@code length} is negative
     74      */
     75     public void skip(@IntRange(from = 0) int length) {
     76         if (length < 0) {
     77             throw new IllegalArgumentException("length cannot be negative: " + length);
     78         }
     79         mCurrentPosition += length;
     80     }
     81 
     82     /**
     83      * Advances the current event position to the next to the last position.
     84      */
     85     public void skipAll() {
     86         mCurrentPosition = mEventSupplier.get().mLength;
     87     }
     88 
     89     /**
     90      * Find the first event that matches the given condition from the current position.
     91      *
     92      * <p>If there is such an event, this method returns such an event without moving the current
     93      * event position.</p>
     94      *
     95      * <p>If there is such an event, this method returns {@link Optional#empty()} without moving the
     96      * current event position.</p>
     97      *
     98      * @param condition the event condition to be matched
     99      * @return {@link Optional#empty()} if there is no such an event. Otherwise the matched event is
    100      *         returned
    101      */
    102     @NonNull
    103     public Optional<ImeEvent> findFirst(Predicate<ImeEvent> condition) {
    104         final ImeEventArray latest = mEventSupplier.get();
    105         int index = mCurrentPosition;
    106         while (true) {
    107             if (index >= latest.mLength) {
    108                 return Optional.empty();
    109             }
    110             if (condition.test(latest.mArray[index])) {
    111                 return Optional.of(latest.mArray[index]);
    112             }
    113             ++index;
    114         }
    115     }
    116 
    117     /**
    118      * Find the first event that matches the given condition from the current position.
    119      *
    120      * <p>If there is such an event, this method returns such an event and set the current event
    121      * position to that event.</p>
    122      *
    123      * <p>If there is such an event, this method returns {@link Optional#empty()} without moving the
    124      * current event position.</p>
    125      *
    126      * @param condition the event condition to be matched
    127      * @return {@link Optional#empty()} if there is no such an event. Otherwise the matched event is
    128      *         returned
    129      */
    130     @NonNull
    131     public Optional<ImeEvent> seekToFirst(Predicate<ImeEvent> condition) {
    132         final ImeEventArray latest = mEventSupplier.get();
    133         while (true) {
    134             if (mCurrentPosition >= latest.mLength) {
    135                 return Optional.empty();
    136             }
    137             if (condition.test(latest.mArray[mCurrentPosition])) {
    138                 return Optional.of(latest.mArray[mCurrentPosition]);
    139             }
    140             ++mCurrentPosition;
    141         }
    142     }
    143 
    144     private static void dumpEvent(@NonNull StringBuilder sb, @NonNull ImeEvent event,
    145             boolean fused) {
    146         final String indentation = getWhiteSpaces(event.getNestLevel() * 2 + 2);
    147         final long wallTime =
    148                 fused ? event.getEnterWallTime() :
    149                         event.isEnterEvent() ? event.getEnterWallTime() : event.getExitWallTime();
    150         sb.append(sSimpleDateTimeFormatter.format(Instant.ofEpochMilli(wallTime)))
    151                 .append("  ")
    152                 .append(String.format("%5d", event.getThreadId()))
    153                 .append(indentation);
    154         sb.append(fused ? "" : event.isEnterEvent() ? "[" : "]");
    155         if (fused || event.isEnterEvent()) {
    156             sb.append(event.getEventName())
    157                     .append(':')
    158                     .append(" args=");
    159             dumpBundle(sb, event.getArguments());
    160         }
    161         sb.append('\n');
    162     }
    163 
    164     /**
    165      * @return Debug info as a {@link String}.
    166      */
    167     public String dump() {
    168         final ImeEventArray latest = mEventSupplier.get();
    169         final StringBuilder sb = new StringBuilder();
    170         sb.append("ImeEventStream:\n");
    171         sb.append("  latest: array[").append(latest.mArray.length).append("] + {\n");
    172         for (int i = 0; i < latest.mLength; ++i) {
    173             // To compress the dump message, if the current event is an enter event and the next
    174             // one is a corresponding exit event, we unify the output.
    175             final boolean fused = areEnterExitPairedMessages(latest, i);
    176             if (i == mCurrentPosition || (fused && ((i + 1) == mCurrentPosition))) {
    177                 sb.append("  ======== CurrentPosition ========  \n");
    178             }
    179             dumpEvent(sb, latest.mArray[fused ? ++i : i], fused);
    180         }
    181         if (mCurrentPosition >= latest.mLength) {
    182             sb.append("  ======== CurrentPosition ========  \n");
    183         }
    184         sb.append("}\n");
    185         return sb.toString();
    186     }
    187 
    188     /**
    189      * @param array event array to be checked
    190      * @param i index to be checked
    191      * @return {@code true} if {@code array.mArray[i]} and {@code array.mArray[i + 1]} are two
    192      *         paired events.
    193      */
    194     private static boolean areEnterExitPairedMessages(@NonNull ImeEventArray array,
    195             @IntRange(from = 0) int i) {
    196         return array.mArray[i] != null
    197                 && array.mArray[i].isEnterEvent()
    198                 && (i + 1) < array.mLength
    199                 && array.mArray[i + 1] != null
    200                 && array.mArray[i].getEventName().equals(array.mArray[i + 1].getEventName())
    201                 && array.mArray[i].getEnterTimestamp() == array.mArray[i + 1].getEnterTimestamp();
    202     }
    203 
    204     /**
    205      * @param length length of the requested white space string
    206      * @return {@link String} object whose length is {@code length}
    207      */
    208     private static String getWhiteSpaces(@IntRange(from = 0) final int length) {
    209         if (length < LONG_LONG_SPACES.length()) {
    210             return LONG_LONG_SPACES.substring(0, length);
    211         }
    212         final char[] indentationChars = new char[length];
    213         Arrays.fill(indentationChars, ' ');
    214         return new String(indentationChars);
    215     }
    216 
    217     private static void dumpBundle(@NonNull StringBuilder sb, @NonNull Bundle bundle) {
    218         sb.append('{');
    219         boolean first = true;
    220         for (String key : bundle.keySet()) {
    221             if (first) {
    222                 first = false;
    223             } else {
    224                 sb.append(' ');
    225             }
    226             final Object object = bundle.get(key);
    227             sb.append(key);
    228             sb.append('=');
    229             if (object instanceof EditorInfo) {
    230                 final EditorInfo info = (EditorInfo) object;
    231                 sb.append("EditorInfo{packageName=").append(info.packageName);
    232                 sb.append(" fieldId=").append(info.fieldId);
    233                 sb.append(" hintText=").append(info.hintText);
    234                 sb.append(" privateImeOptions=").append(info.privateImeOptions);
    235                 sb.append("}");
    236             } else {
    237                 sb.append(object);
    238             }
    239         }
    240         sb.append('}');
    241     }
    242 
    243     static class ImeEventArray {
    244         @NonNull
    245         public final ImeEvent[] mArray;
    246         public final int mLength;
    247         ImeEventArray(ImeEvent[] array, int length) {
    248             mArray = array;
    249             mLength = length;
    250         }
    251     }
    252 }
    253