Introduce ImeEventStream
This CL introduces ImeEventStream that represents the event stream of
ImeEvent objects. This is similar to read-only file stream, except
that some of methods do support timeout operation.
try(MockImeSession imeSession = MockImeSession.create(
InstrumentationRegistry.getContext(),
InstrumentationRegistry.getInstrumentation().getUiAutomation(),
new ImeSettings.Builder()) {
final ImeEventStream stream = imeSession.openEventStream();
final Optional<ImeEvent> result = stream.seekToFirst(event -> {
if (!TextUtils.equals("bindInput", event.getEventName())) {
return false;
}
final InputBinding binding =
event.getArguments().getParcelable("binding");
return binding.getPid() == Process.myPid();
}, TIMEOUT);
// Create a new stream that has the same stream position.
final ImeEventStream newStream = stream.copy();
// Moving the current position of "stream" does not affect the
// current position of "newStream".
stream.skip(1);
}
Further operations will be added in subsequent CLs.
Bug: 69845539
Test: atest CtsInputMethodTestCases
Change-Id: I1595f0747621871b7cc47e905b2ad3931779955a
diff --git a/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeEventStream.java b/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeEventStream.java
new file mode 100644
index 0000000..3e93c48
--- /dev/null
+++ b/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeEventStream.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.cts.mockime;
+
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * A utility class that provides basic query operations and wait primitives for a series of
+ * {@link ImeEvent} sent from the {@link MockIme}.
+ *
+ * <p>All public methods are not thread-safe.</p>
+ */
+public final class ImeEventStream {
+
+ @NonNull
+ private final Supplier<ImeEventArray> mEventSupplier;
+ private int mCurrentPosition;
+
+ ImeEventStream(@NonNull Supplier<ImeEventArray> supplier) {
+ this(supplier, 0 /* position */);
+ }
+
+ private ImeEventStream(@NonNull Supplier<ImeEventArray> supplier, int position) {
+ mEventSupplier = supplier;
+ mCurrentPosition = position;
+ }
+
+ /**
+ * Create a copy that starts from the same event position of this stream. Once a copy is created
+ * further event position change on this stream will not affect the copy.
+ *
+ * @return A new copy of this stream
+ */
+ public ImeEventStream copy() {
+ return new ImeEventStream(mEventSupplier, mCurrentPosition);
+ }
+
+ /**
+ * Advances the current event position by skipping events.
+ *
+ * @param length number of events to be skipped
+ * @throws IllegalArgumentException {@code length} is negative
+ */
+ public void skip(@IntRange(from = 0) int length) {
+ if (length < 0) {
+ throw new IllegalArgumentException("length cannot be negative: " + length);
+ }
+ mCurrentPosition += length;
+ }
+
+ /**
+ * Find the first event that matches the given condition from the current position.
+ *
+ * <p>If there is such an event, this method returns such an event without moving the current
+ * event position.</p>
+ *
+ * <p>If there is such an event, this method returns {@link Optional#empty()} without moving the
+ * current event position.</p>
+ *
+ * @param condition the event condition to be matched
+ * @return {@link Optional#empty()} if there is no such an event. Otherwise the matched event is
+ * returned
+ */
+ @NonNull
+ public Optional<ImeEvent> findFirst(Predicate<ImeEvent> condition) {
+ final ImeEventArray latest = mEventSupplier.get();
+ int index = mCurrentPosition;
+ while (true) {
+ if (index >= latest.mLength) {
+ return Optional.empty();
+ }
+ if (condition.test(latest.mArray[index])) {
+ return Optional.of(latest.mArray[index]);
+ }
+ ++index;
+ }
+ }
+
+ /**
+ * Find the first event that matches the given condition from the current position.
+ *
+ * <p>If there is such an event, this method returns such an event and set the current event
+ * position to that event.</p>
+ *
+ * <p>If there is such an event, this method returns {@link Optional#empty()} without moving the
+ * current event position.</p>
+ *
+ * @param condition the event condition to be matched
+ * @return {@link Optional#empty()} if there is no such an event. Otherwise the matched event is
+ * returned
+ */
+ @NonNull
+ public Optional<ImeEvent> seekToFirst(Predicate<ImeEvent> condition) {
+ final ImeEventArray latest = mEventSupplier.get();
+ while (true) {
+ if (mCurrentPosition >= latest.mLength) {
+ return Optional.empty();
+ }
+ if (condition.test(latest.mArray[mCurrentPosition])) {
+ return Optional.of(latest.mArray[mCurrentPosition]);
+ }
+ ++mCurrentPosition;
+ }
+ }
+
+ final static class ImeEventArray {
+ @NonNull
+ public final ImeEvent[] mArray;
+ public final int mLength;
+ public ImeEventArray(ImeEvent[] array, int length) {
+ mArray = array;
+ mLength = length;
+ }
+ }
+}
diff --git a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockImeSession.java b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockImeSession.java
index a56717e..ea6b465 100644
--- a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockImeSession.java
+++ b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockImeSession.java
@@ -33,6 +33,7 @@
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.provider.Settings;
+import android.support.annotation.GuardedBy;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
@@ -45,7 +46,6 @@
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
@@ -69,9 +69,45 @@
private final HandlerThread mHandlerThread = new HandlerThread("EventReceiver");
- private static final class MockImeEventReceiver extends BroadcastReceiver {
+ private final static class EventStore {
+ private final static int INITIAL_ARRAY_SIZE = 32;
+
@NonNull
- private final ArrayList<ImeEvent> mCurrentEventStore = new ArrayList<>();
+ public final ImeEvent[] mArray;
+ public int mLength;
+
+ public EventStore() {
+ mArray = new ImeEvent[INITIAL_ARRAY_SIZE];
+ mLength = 0;
+ }
+
+ public EventStore(EventStore src, int newLength) {
+ mArray = new ImeEvent[newLength];
+ mLength = src.mLength;
+ System.arraycopy(src.mArray, 0, mArray, 0, src.mLength);
+ }
+
+ public EventStore add(ImeEvent event) {
+ if (mLength + 1 <= mArray.length) {
+ mArray[mLength] = event;
+ ++mLength;
+ return this;
+ } else {
+ return new EventStore(this, mLength * 2).add(event);
+ }
+ }
+
+ public ImeEventStream.ImeEventArray takeSnapshot() {
+ return new ImeEventStream.ImeEventArray(mArray, mLength);
+ }
+ }
+
+ private static final class MockImeEventReceiver extends BroadcastReceiver {
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ @NonNull
+ private EventStore mCurrentEventStore = new EventStore();
@NonNull
private final String mActionName;
@@ -83,13 +119,25 @@
@Override
public void onReceive(Context context, Intent intent) {
if (TextUtils.equals(mActionName, intent.getAction())) {
- mCurrentEventStore.add(ImeEvent.fromBundle(intent.getExtras()));
+ synchronized (mLock) {
+ mCurrentEventStore =
+ mCurrentEventStore.add(ImeEvent.fromBundle(intent.getExtras()));
+ }
+ }
+ }
+
+ public ImeEventStream.ImeEventArray takeEventSnapshot() {
+ synchronized (mLock) {
+ return mCurrentEventStore.takeSnapshot();
}
}
}
private final MockImeEventReceiver mEventReceiver =
new MockImeEventReceiver(mImeEventActionName);
+ private final ImeEventStream mEventStream =
+ new ImeEventStream(mEventReceiver::takeEventSnapshot);
+
private static String executeShellCommand(
@NonNull UiAutomation uiAutomation, @NonNull String command) {
try (ParcelFileDescriptor.AutoCloseInputStream in =
@@ -204,6 +252,14 @@
}
/**
+ * @return {@link ImeEventStream} object that stores events sent from {@link MockIme} since the
+ * session is created.
+ */
+ public ImeEventStream openEventStream() {
+ return mEventStream.copy();
+ }
+
+ /**
* Closes the active session and de-selects {@link MockIme}. Currently which IME will be
* selected next is up to the system.
*/