Add CTS to verify IMS.onStartInput() is called once.
Also added mechanism to serialize/deserialize IMS event extras.
Test: cts-tradefed run singleCommand cts-dev --module
CtsInputMethodServiceHostTestCases --test
android.inputmethodservice.cts.hostside.InputMethodServiceLifecycleTest#testOnStartInputCalledOnce
Bug: 35599628
Change-Id: I5fd7d8a535d66b7599bb03d7192668d00e10d81c
diff --git a/hostsidetests/inputmethodservice/common/src/android/inputmethodservice/cts/common/DeviceEventConstants.java b/hostsidetests/inputmethodservice/common/src/android/inputmethodservice/cts/common/DeviceEventConstants.java
index 9927ca9..307693a 100644
--- a/hostsidetests/inputmethodservice/common/src/android/inputmethodservice/cts/common/DeviceEventConstants.java
+++ b/hostsidetests/inputmethodservice/common/src/android/inputmethodservice/cts/common/DeviceEventConstants.java
@@ -50,6 +50,12 @@
public static final String EXTRA_EVENT_SENDER = "event_sender";
/**
+ * Intent extra key for Event parameters like
+ * {@link DeviceEventTypeParam#ON_START_INPUT_RESTARTING}
+ */
+ public static final String EXTRA_EVENT_PARAMS = "event_params";
+
+ /**
* Intent extra key for what type a device event is. Values are {@link DeviceEventType#name()}.
*
* @see android.content.Intent#putExtra(String,String)
@@ -67,6 +73,29 @@
public static final String EXTRA_EVENT_TIME = "event_time";
/**
+ * Parameter for {@link DeviceEventType}.
+ */
+ public enum DeviceEventTypeParam {
+
+ /**
+ * Param for {@link DeviceEventType#ON_START_INPUT}. Represents if IME is restarting.
+ */
+ ON_START_INPUT_RESTARTING(DeviceEventType.ON_START_INPUT, "onStartInput.restarting");
+
+ private final DeviceEventType mType;
+ private final String mName;
+
+ DeviceEventTypeParam(DeviceEventType type, String name) {
+ mType = type;
+ mName = name;
+ }
+
+ public String getName() {
+ return mName;
+ }
+ }
+
+ /**
* Types of device event, a value of {@link #EXTRA_EVENT_TYPE}.
*/
public enum DeviceEventType {
diff --git a/hostsidetests/inputmethodservice/common/src/android/inputmethodservice/cts/common/EventProviderConstants.java b/hostsidetests/inputmethodservice/common/src/android/inputmethodservice/cts/common/EventProviderConstants.java
index 172e1a7..9288051 100644
--- a/hostsidetests/inputmethodservice/common/src/android/inputmethodservice/cts/common/EventProviderConstants.java
+++ b/hostsidetests/inputmethodservice/common/src/android/inputmethodservice/cts/common/EventProviderConstants.java
@@ -58,6 +58,9 @@
// This is constants holding class, can't instantiate.
private EventTableConstants() {}
+ /** Column name of the table that holds Event extras in json format. */
+ public static final String EXTRAS = "extras";
+
/** Name of the table in content provider and database. */
public static final String NAME = "events";
diff --git a/hostsidetests/inputmethodservice/common/src/android/inputmethodservice/cts/common/test/DeviceTestConstants.java b/hostsidetests/inputmethodservice/common/src/android/inputmethodservice/cts/common/test/DeviceTestConstants.java
index ad48a46..aa90e11 100644
--- a/hostsidetests/inputmethodservice/common/src/android/inputmethodservice/cts/common/test/DeviceTestConstants.java
+++ b/hostsidetests/inputmethodservice/common/src/android/inputmethodservice/cts/common/test/DeviceTestConstants.java
@@ -47,4 +47,6 @@
= "testSearchView_giveFocusShowIme1";
public static final String TEST_SEARCH_VIEW_SET_QUERY_HIDE_IME1
= "testSearchView_setQueryHideIme1";
+ public static final String TEST_ON_START_INPUT_CALLED_ONCE_IME1
+ = "testOnStartInputCalledOnceIme1";
}
diff --git a/hostsidetests/inputmethodservice/deviceside/devicetest/src/android/inputmethodservice/cts/devicetest/InputMethodServiceDeviceTest.java b/hostsidetests/inputmethodservice/deviceside/devicetest/src/android/inputmethodservice/cts/devicetest/InputMethodServiceDeviceTest.java
index 8278943..e5613ff 100644
--- a/hostsidetests/inputmethodservice/deviceside/devicetest/src/android/inputmethodservice/cts/devicetest/InputMethodServiceDeviceTest.java
+++ b/hostsidetests/inputmethodservice/deviceside/devicetest/src/android/inputmethodservice/cts/devicetest/InputMethodServiceDeviceTest.java
@@ -16,6 +16,10 @@
package android.inputmethodservice.cts.devicetest;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
import static android.inputmethodservice.cts.DeviceEvent.isFrom;
import static android.inputmethodservice.cts.DeviceEvent.isNewerThan;
import static android.inputmethodservice.cts.DeviceEvent.isType;
@@ -35,6 +39,7 @@
import android.app.Activity;
import android.inputmethodservice.cts.DeviceEvent;
import android.inputmethodservice.cts.common.DeviceEventConstants.DeviceEventType;
+import android.inputmethodservice.cts.common.DeviceEventConstants.DeviceEventTypeParam;
import android.inputmethodservice.cts.common.Ime1Constants;
import android.inputmethodservice.cts.common.Ime2Constants;
import android.inputmethodservice.cts.common.test.DeviceTestConstants;
@@ -48,10 +53,12 @@
import org.junit.runner.RunWith;
import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.IntFunction;
import java.util.function.Predicate;
import java.util.stream.Collector;
+import java.util.stream.Collectors;
@RunWith(AndroidJUnit4.class)
public class InputMethodServiceDeviceTest {
@@ -185,6 +192,38 @@
TIMEOUT, "CtsInputMethod1.hideSoftInput is called");
}
+ @Test
+ public void testOnStartInputCalledOnceIme1() throws Exception {
+ final TestHelper helper = new TestHelper(
+ getClass(), DeviceTestConstants.TEST_ON_START_INPUT_CALLED_ONCE_IME1);
+
+ helper.launchActivity(DeviceTestConstants.PACKAGE, DeviceTestConstants.TEST_ACTIVITY_CLASS);
+ helper.findUiObject(R.id.text_entry).click();
+
+ // we should've only one onStartInput call.
+ pollingCheck(() -> helper.queryAllEvents()
+ .collect(startingFrom(helper.isStartOfTest()))
+ .filter(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT)))
+ .findAny()
+ .isPresent(),
+ TIMEOUT, "CtsInputMethod1.onStartInput is called");
+ List<DeviceEvent> startInputEvents = helper.queryAllEvents()
+ .collect(startingFrom(helper.isStartOfTest()))
+ .filter(isFrom(Ime1Constants.CLASS).and(isType(ON_START_INPUT)))
+ .collect(Collectors.toList());
+
+ assertEquals("CtsInputMethod1.onStartInput is called exactly once",
+ startInputEvents.size(),
+ 1);
+
+ // check if that single event didn't cause IME restart.
+ final DeviceEvent event = startInputEvents.get(0);
+ Boolean isRestarting = DeviceEvent.getEventParamBoolean(
+ DeviceEventTypeParam.ON_START_INPUT_RESTARTING, event);
+ assertTrue(isRestarting != null);
+ assertFalse(isRestarting);
+ }
+
/**
* Build stream collector of {@link DeviceEvent} collecting sequence that elements have
* specified types.
diff --git a/hostsidetests/inputmethodservice/deviceside/lib/Android.mk b/hostsidetests/inputmethodservice/deviceside/lib/Android.mk
index d9d4724..6977d6b 100644
--- a/hostsidetests/inputmethodservice/deviceside/lib/Android.mk
+++ b/hostsidetests/inputmethodservice/deviceside/lib/Android.mk
@@ -19,6 +19,7 @@
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_STATIC_JAVA_LIBRARIES := \
android-support-annotations \
+ json \
CtsInputMethodServiceCommon
LOCAL_MODULE_TAGS := tests
diff --git a/hostsidetests/inputmethodservice/deviceside/lib/src/android/inputmethodservice/cts/DeviceEvent.java b/hostsidetests/inputmethodservice/deviceside/lib/src/android/inputmethodservice/cts/DeviceEvent.java
index b6098f9..ff412ef 100644
--- a/hostsidetests/inputmethodservice/deviceside/lib/src/android/inputmethodservice/cts/DeviceEvent.java
+++ b/hostsidetests/inputmethodservice/deviceside/lib/src/android/inputmethodservice/cts/DeviceEvent.java
@@ -17,17 +17,20 @@
package android.inputmethodservice.cts;
import static android.inputmethodservice.cts.common.DeviceEventConstants.ACTION_DEVICE_EVENT;
+import static android.inputmethodservice.cts.common.DeviceEventConstants.EXTRA_EVENT_PARAMS;
import static android.inputmethodservice.cts.common.DeviceEventConstants.EXTRA_EVENT_TIME;
import static android.inputmethodservice.cts.common.DeviceEventConstants.EXTRA_EVENT_TYPE;
import static android.inputmethodservice.cts.common.DeviceEventConstants.EXTRA_EVENT_SENDER;
import static android.inputmethodservice.cts.common.DeviceEventConstants.RECEIVER_CLASS;
import static android.inputmethodservice.cts.common.DeviceEventConstants.RECEIVER_PACKAGE;
+
import android.content.ContentValues;
import android.content.Intent;
import android.database.Cursor;
import android.inputmethodservice.cts.common.DeviceEventConstants;
import android.inputmethodservice.cts.common.DeviceEventConstants.DeviceEventType;
+import android.inputmethodservice.cts.common.DeviceEventConstants.DeviceEventTypeParam;
import android.inputmethodservice.cts.common.EventProviderConstants.EventTableConstants;
import android.inputmethodservice.cts.common.test.TestInfo;
import android.inputmethodservice.cts.db.Entity;
@@ -35,8 +38,16 @@
import android.inputmethodservice.cts.db.Table;
import android.os.SystemClock;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
import android.util.Log;
+import com.android.json.stream.JsonReader;
+import com.android.json.stream.JsonWriter;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
import java.util.function.Predicate;
import java.util.stream.Stream;
@@ -51,21 +62,85 @@
public static final Table<DeviceEvent> TABLE = new DeviceEventTable(EventTableConstants.NAME);
+ public static IntentBuilder builder() {
+ return new IntentBuilder();
+ }
+
/**
- * Create an intent to send a device event.
- * @param sender an event sender.
- * @param type an event type defined at {@link DeviceEventType}.
- * @return an intent that has event {@code sender}, {@code type}, time from
- * {@link SystemClock#uptimeMillis()}, and target component of event receiver.
+ * Builder to create an intent to send a device event.
+ * The built intent that has event {@code sender}, {@code type}, {@code paramsString}, time from
+ * {@link SystemClock#uptimeMillis()}, and target component of event receiver.
+ *
*/
- public static Intent newDeviceEventIntent(@NonNull final String sender,
- @NonNull final DeviceEventType type) {
- return new Intent()
- .setAction(ACTION_DEVICE_EVENT)
- .setClassName(RECEIVER_PACKAGE, RECEIVER_CLASS)
- .putExtra(EXTRA_EVENT_SENDER, sender)
- .putExtra(EXTRA_EVENT_TYPE, type.name())
- .putExtra(EXTRA_EVENT_TIME, SystemClock.uptimeMillis());
+ public static final class IntentBuilder {
+ String mSender;
+ DeviceEventType mType;
+ JsonWriter mJsonWriter;
+ StringWriter mStringWriter;
+
+ /**
+ * @param type an event type defined at {@link DeviceEventType}.
+ */
+ public IntentBuilder setType(DeviceEventType type) {
+ mType = type;
+ return this;
+ }
+
+ /**
+ * @param sender an event sender.
+ */
+ public void setSender(String sender) {
+ mSender = sender;
+ }
+
+ public IntentBuilder with(DeviceEventTypeParam eventParam, boolean value) {
+ appendToJson(eventParam, value);
+ return this;
+ }
+
+ public Intent build() {
+ Intent intent = new Intent()
+ .setAction(ACTION_DEVICE_EVENT)
+ .setClassName(RECEIVER_PACKAGE, RECEIVER_CLASS)
+ .putExtra(EXTRA_EVENT_SENDER, mSender)
+ .putExtra(EXTRA_EVENT_TYPE, mType.name())
+ .putExtra(EXTRA_EVENT_PARAMS, getJsonString())
+ .putExtra(EXTRA_EVENT_TIME, SystemClock.uptimeMillis());
+
+ mJsonWriter = null;
+ mStringWriter = null;
+ return intent;
+ }
+
+ private String getJsonString() {
+ if (mJsonWriter == null) {
+ return "";
+ }
+ try {
+ mJsonWriter.endObject();
+ mJsonWriter.flush();
+ } catch (IOException e) {
+ throw new RuntimeException("IntentBuilder.getJsonString() failed.", e);
+ }
+ return mStringWriter.toString();
+ }
+
+ private void appendToJson(DeviceEventTypeParam eventParam, boolean value) {
+ final String key = eventParam.getName();
+ if (TextUtils.isEmpty(key)) {
+ return;
+ }
+ try {
+ if (mJsonWriter == null) {
+ mStringWriter = new StringWriter();
+ mJsonWriter = new JsonWriter(mStringWriter);
+ mJsonWriter.beginObject();
+ }
+ mJsonWriter.name(key).value(value);
+ } catch (IOException e) {
+ throw new RuntimeException("IntentBuilder.appendToJson() failed.", e);
+ }
+ }
}
/**
@@ -93,7 +168,16 @@
"Intent must have " + EXTRA_EVENT_TIME + ": " + intent);
}
- return new DeviceEvent(sender, type, intent.getLongExtra(EXTRA_EVENT_TIME, 0L));
+ String paramsString = intent.getStringExtra(DeviceEventConstants.EXTRA_EVENT_PARAMS);
+ if (paramsString == null) {
+ paramsString = "";
+ }
+
+ return new DeviceEvent(
+ sender,
+ type,
+ paramsString,
+ intent.getLongExtra(EXTRA_EVENT_TIME, 0L));
}
/**
@@ -158,13 +242,21 @@
public final DeviceEventType type;
/**
+ * Event parameters formatted as json string.
+ * e.g. {@link DeviceEventTypeParam#ON_START_INPUT_RESTARTING}
+ */
+ public final String paramsString;
+
+ /**
* Event time, value is from {@link SystemClock#uptimeMillis()}.
*/
public final long time;
- private DeviceEvent(final String sender, final DeviceEventType type, final long time) {
+ private DeviceEvent(
+ final String sender, final DeviceEventType type, String paramsString, final long time) {
this.sender = sender;
this.type = type;
+ this.paramsString = paramsString;
this.time = time;
}
@@ -174,6 +266,37 @@
}
/**
+ * @param eventParam {@link DeviceEventTypeParam} to look for.
+ * @param event {@link DeviceEvent} to look in.
+ * @return Event parameter for provided key. If key is not found in
+ * {@link DeviceEvent#paramsString}, null is returned.
+ *
+ * TODO: Support other primitive and custom types.
+ */
+ @Nullable
+ public static Boolean getEventParamBoolean(
+ DeviceEventTypeParam eventParam, final DeviceEvent event) {
+ StringReader stringReader = new StringReader(event.paramsString);
+ JsonReader reader = new JsonReader(stringReader);
+
+ try {
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String name = reader.nextName();
+ if (name.equals(eventParam.getName())) {
+ Boolean value = reader.nextBoolean();
+ reader.endObject();
+ return value;
+ }
+ }
+ reader.endObject();
+ } catch (IOException e) {
+ throw new RuntimeException("DeviceEvent.getEventParamBoolean() failed.", e);
+ }
+ return null;
+ }
+
+ /**
* Abstraction of device event table in database.
*/
private static final class DeviceEventTable extends Table<DeviceEvent> {
@@ -183,16 +306,19 @@
private final Field SENDER;
private final Field TYPE;
private final Field TIME;
+ private final Field PARAMS;
private DeviceEventTable(final String name) {
super(name, new Entity.Builder<DeviceEvent>()
.addField(EventTableConstants.SENDER, Cursor.FIELD_TYPE_STRING)
.addField(EventTableConstants.TYPE, Cursor.FIELD_TYPE_STRING)
.addField(EventTableConstants.TIME, Cursor.FIELD_TYPE_INTEGER)
+ .addField(EventTableConstants.EXTRAS, Cursor.FIELD_TYPE_STRING)
.build());
SENDER = getField(EventTableConstants.SENDER);
TYPE = getField(EventTableConstants.TYPE);
TIME = getField(EventTableConstants.TIME);
+ PARAMS = getField(EventTableConstants.EXTRAS);
}
@Override
@@ -200,6 +326,7 @@
final ContentValues values = new ContentValues();
SENDER.putString(values, event.sender);
TYPE.putString(values, event.type.name());
+ PARAMS.putString(values, event.paramsString);
TIME.putLong(values, event.time);
return values;
}
@@ -214,6 +341,7 @@
final DeviceEvent event = new DeviceEvent(
SENDER.getString(cursor),
DeviceEventType.valueOf(TYPE.getString(cursor)),
+ PARAMS.getString(cursor),
TIME.getLong(cursor));
builder.accept(event);
if (DEBUG_STREAM) {
diff --git a/hostsidetests/inputmethodservice/deviceside/lib/src/android/inputmethodservice/cts/ime/CtsBaseInputMethod.java b/hostsidetests/inputmethodservice/deviceside/lib/src/android/inputmethodservice/cts/ime/CtsBaseInputMethod.java
index 05a7a1d..b37873c 100644
--- a/hostsidetests/inputmethodservice/deviceside/lib/src/android/inputmethodservice/cts/ime/CtsBaseInputMethod.java
+++ b/hostsidetests/inputmethodservice/deviceside/lib/src/android/inputmethodservice/cts/ime/CtsBaseInputMethod.java
@@ -25,11 +25,11 @@
import static android.inputmethodservice.cts.common.DeviceEventConstants.DeviceEventType.ON_START_INPUT_VIEW;
import static android.inputmethodservice.cts.common.DeviceEventConstants.DeviceEventType.SHOW_SOFT_INPUT;
-import android.content.Intent;
import android.inputmethodservice.InputMethodService;
import android.inputmethodservice.cts.DeviceEvent;
-import android.inputmethodservice.cts.common.DeviceEventConstants.DeviceEventType;
+import android.inputmethodservice.cts.common.DeviceEventConstants.DeviceEventTypeParam;
import android.inputmethodservice.cts.ime.ImeCommandReceiver.ImeCommandCallbacks;
+
import android.os.ResultReceiver;
import android.util.Log;
import android.view.inputmethod.EditorInfo;
@@ -48,7 +48,7 @@
private class CtsInputMethodImpl extends InputMethodImpl {
@Override
public void showSoftInput(int flags, ResultReceiver resultReceiver) {
- sendEvent(SHOW_SOFT_INPUT);
+ sendEvent(DeviceEvent.builder().setType(SHOW_SOFT_INPUT));
if (DEBUG) {
Log.d(mLogTag, "showSoftInput called");
}
@@ -57,7 +57,7 @@
@Override
public void hideSoftInput(int flags, ResultReceiver resultReceiver) {
- sendEvent(HIDE_SOFT_INPUT);
+ sendEvent(DeviceEvent.builder().setType(HIDE_SOFT_INPUT));
if (DEBUG) {
Log.d(mLogTag, "hideSoftInput called");
}
@@ -71,7 +71,7 @@
if (DEBUG) {
Log.d(mLogTag, "onCreate:");
}
- sendEvent(ON_CREATE);
+ sendEvent(DeviceEvent.builder().setType(ON_CREATE));
super.onCreate();
@@ -85,8 +85,10 @@
+ " editorInfo=" + editorInfo
+ " restarting=" + restarting);
}
- sendEvent(ON_START_INPUT, editorInfo, restarting);
+ sendEvent(DeviceEvent.builder()
+ .setType(ON_START_INPUT)
+ .with(DeviceEventTypeParam.ON_START_INPUT_RESTARTING, restarting));
super.onStartInput(editorInfo, restarting);
}
@@ -97,7 +99,8 @@
+ " editorInfo=" + editorInfo
+ " restarting=" + restarting);
}
- sendEvent(ON_START_INPUT_VIEW, editorInfo, restarting);
+
+ sendEvent(DeviceEvent.builder().setType(ON_START_INPUT_VIEW));
super.onStartInputView(editorInfo, restarting);
}
@@ -107,7 +110,7 @@
if (DEBUG) {
Log.d(mLogTag, "onFinishInputView: finishingInput=" + finishingInput);
}
- sendEvent(ON_FINISH_INPUT_VIEW, finishingInput);
+ sendEvent(DeviceEvent.builder().setType(ON_FINISH_INPUT_VIEW));
super.onFinishInputView(finishingInput);
}
@@ -117,7 +120,7 @@
if (DEBUG) {
Log.d(mLogTag, "onFinishInput:");
}
- sendEvent(ON_FINISH_INPUT);
+ sendEvent(DeviceEvent.builder().setType(ON_FINISH_INPUT));
super.onFinishInput();
}
@@ -127,7 +130,7 @@
if (DEBUG) {
Log.d(mLogTag, "onDestroy:");
}
- sendEvent(ON_DESTROY);
+ sendEvent(DeviceEvent.builder().setType(ON_DESTROY));
super.onDestroy();
@@ -175,10 +178,8 @@
}
}
- private void sendEvent(final DeviceEventType type, final Object... args) {
- final String sender = getClass().getName();
- final Intent intent = DeviceEvent.newDeviceEventIntent(sender, type);
- // TODO: Send arbitrary {@code args} in {@code intent}.
- sendBroadcast(intent);
+ private void sendEvent(final DeviceEvent.IntentBuilder intentBuilder) {
+ intentBuilder.setSender(getClass().getName());
+ sendBroadcast(intentBuilder.build());
}
}
diff --git a/hostsidetests/inputmethodservice/hostside/src/android/inputmethodservice/cts/hostside/InputMethodServiceLifecycleTest.java b/hostsidetests/inputmethodservice/hostside/src/android/inputmethodservice/cts/hostside/InputMethodServiceLifecycleTest.java
index 2f76357..02c3e55 100644
--- a/hostsidetests/inputmethodservice/hostside/src/android/inputmethodservice/cts/hostside/InputMethodServiceLifecycleTest.java
+++ b/hostsidetests/inputmethodservice/hostside/src/android/inputmethodservice/cts/hostside/InputMethodServiceLifecycleTest.java
@@ -122,6 +122,17 @@
assertTrue(runDeviceTestMethod(testSetQueryHideIme1));
}
+ @Test
+ public void testOnStartInputCalledOnce() throws Exception {
+ installAndSetIme1();
+
+ final TestInfo testSetQueryHideIme1 = new TestInfo(DeviceTestConstants.PACKAGE,
+ DeviceTestConstants.TEST_CLASS,
+ DeviceTestConstants.TEST_ON_START_INPUT_CALLED_ONCE_IME1);
+ sendTestStartEvent(testSetQueryHideIme1);
+ assertTrue(runDeviceTestMethod(testSetQueryHideIme1));
+ }
+
private void installAndSetIme1() throws Exception {
final TestInfo testCreateIme1 = new TestInfo(DeviceTestConstants.PACKAGE,
DeviceTestConstants.TEST_CLASS, DeviceTestConstants.TEST_CREATE_IME1);