aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--java/src/com/android/inputmethod/research/MotionEventReader.java253
-rw-r--r--java/src/com/android/inputmethod/research/Replayer.java20
-rw-r--r--tests/src/com/android/inputmethod/research/MotionEventReaderTests.java169
3 files changed, 421 insertions, 21 deletions
diff --git a/java/src/com/android/inputmethod/research/MotionEventReader.java b/java/src/com/android/inputmethod/research/MotionEventReader.java
index 26a1d7f55..e59adfa19 100644
--- a/java/src/com/android/inputmethod/research/MotionEventReader.java
+++ b/java/src/com/android/inputmethod/research/MotionEventReader.java
@@ -19,6 +19,8 @@ package com.android.inputmethod.research;
import android.util.JsonReader;
import android.util.Log;
import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
import com.android.inputmethod.latin.define.ProductionFlag;
@@ -33,6 +35,14 @@ import java.util.ArrayList;
public class MotionEventReader {
private static final String TAG = MotionEventReader.class.getSimpleName();
private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+ // Assumes that MotionEvent.ACTION_MASK does not have all bits set.`
+ private static final int UNINITIALIZED_ACTION = ~MotionEvent.ACTION_MASK;
+ // No legitimate int is negative
+ private static final int UNINITIALIZED_INT = -1;
+ // No legitimate long is negative
+ private static final long UNINITIALIZED_LONG = -1L;
+ // No legitimate float is negative
+ private static final float UNINITIALIZED_FLOAT = -1.0f;
public ReplayData readMotionEventData(final File file) {
final ReplayData replayData = new ReplayData();
@@ -55,19 +65,82 @@ public class MotionEventReader {
static class ReplayData {
final ArrayList<Integer> mActions = new ArrayList<Integer>();
- final ArrayList<Integer> mXCoords = new ArrayList<Integer>();
- final ArrayList<Integer> mYCoords = new ArrayList<Integer>();
+ final ArrayList<PointerProperties[]> mPointerPropertiesArrays
+ = new ArrayList<PointerProperties[]>();
+ final ArrayList<PointerCoords[]> mPointerCoordsArrays = new ArrayList<PointerCoords[]>();
final ArrayList<Long> mTimes = new ArrayList<Long>();
}
- private void readLogStatement(final JsonReader jsonReader, final ReplayData replayData)
- throws IOException {
+ /**
+ * Read motion data from a logStatement and store it in {@code replayData}.
+ *
+ * Two kinds of logStatements can be read. In the first variant, the MotionEvent data is
+ * represented as attributes at the top level like so:
+ *
+ * <pre>
+ * {
+ * "_ct": 1359590400000,
+ * "_ut": 4381933,
+ * "_ty": "MotionEvent",
+ * "action": "UP",
+ * "isLoggingRelated": false,
+ * "x": 100,
+ * "y": 200
+ * }
+ * </pre>
+ *
+ * In the second variant, there is a separate attribute for the MotionEvent that includes
+ * historical data if present:
+ *
+ * <pre>
+ * {
+ * "_ct": 135959040000,
+ * "_ut": 4382702,
+ * "_ty": "MotionEvent",
+ * "action": "MOVE",
+ * "isLoggingRelated": false,
+ * "motionEvent": {
+ * "pointerIds": [
+ * 0
+ * ],
+ * "xyt": [
+ * {
+ * "t": 4382551,
+ * "d": [
+ * {
+ * "x": 141.25,
+ * "y": 151.8485107421875,
+ * "toma": 101.82337188720703,
+ * "tomi": 101.82337188720703,
+ * "o": 0.0
+ * }
+ * ]
+ * },
+ * {
+ * "t": 4382559,
+ * "d": [
+ * {
+ * "x": 140.7266082763672,
+ * "y": 151.8485107421875,
+ * "toma": 101.82337188720703,
+ * "tomi": 101.82337188720703,
+ * "o": 0.0
+ * }
+ * ]
+ * }
+ * ]
+ * }
+ * },
+ * </pre>
+ */
+ /* package for test */ void readLogStatement(final JsonReader jsonReader,
+ final ReplayData replayData) throws IOException {
String logStatementType = null;
- Integer actionType = null;
- Integer x = null;
- Integer y = null;
- Long time = null;
- boolean loggingRelated = false;
+ int actionType = UNINITIALIZED_ACTION;
+ int x = UNINITIALIZED_INT;
+ int y = UNINITIALIZED_INT;
+ long time = UNINITIALIZED_LONG;
+ boolean isLoggingRelated = false;
jsonReader.beginObject();
while (jsonReader.hasNext()) {
@@ -90,7 +163,18 @@ public class MotionEventReader {
actionType = MotionEvent.ACTION_MOVE;
}
} else if (key.equals("loggingRelated")) {
- loggingRelated = jsonReader.nextBoolean();
+ isLoggingRelated = jsonReader.nextBoolean();
+ } else if (logStatementType != null && logStatementType.equals("MotionEvent")
+ && key.equals("motionEvent")) {
+ if (actionType == UNINITIALIZED_ACTION) {
+ Log.e(TAG, "no actionType assigned in MotionEvent json");
+ }
+ // Second variant of LogStatement.
+ if (isLoggingRelated) {
+ jsonReader.skipValue();
+ } else {
+ readEmbeddedMotionEvent(jsonReader, replayData, actionType);
+ }
} else {
if (DEBUG) {
Log.w(TAG, "Unknown JSON key in LogStatement: " + key);
@@ -100,14 +184,149 @@ public class MotionEventReader {
}
jsonReader.endObject();
- if (logStatementType != null && time != null && x != null && y != null && actionType != null
- && logStatementType.equals("MotionEvent")
- && !loggingRelated) {
- replayData.mActions.add(actionType);
- replayData.mXCoords.add(x);
- replayData.mYCoords.add(y);
- replayData.mTimes.add(time);
+ if (logStatementType != null && time != UNINITIALIZED_LONG && x != UNINITIALIZED_INT
+ && y != UNINITIALIZED_INT && actionType != UNINITIALIZED_ACTION
+ && logStatementType.equals("MotionEvent") && !isLoggingRelated) {
+ // First variant of LogStatement.
+ final PointerProperties pointerProperties = new PointerProperties();
+ pointerProperties.id = 0;
+ pointerProperties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
+ final PointerProperties[] pointerPropertiesArray = {
+ pointerProperties
+ };
+ final PointerCoords pointerCoords = new PointerCoords();
+ pointerCoords.x = x;
+ pointerCoords.y = y;
+ pointerCoords.pressure = 1.0f;
+ pointerCoords.size = 1.0f;
+ final PointerCoords[] pointerCoordsArray = {
+ pointerCoords
+ };
+ addMotionEventData(replayData, actionType, time, pointerPropertiesArray,
+ pointerCoordsArray);
+ }
+ }
+
+ private void readEmbeddedMotionEvent(final JsonReader jsonReader, final ReplayData replayData,
+ final int actionType) throws IOException {
+ jsonReader.beginObject();
+ PointerProperties[] pointerPropertiesArray = null;
+ while (jsonReader.hasNext()) { // pointerIds/xyt
+ final String name = jsonReader.nextName();
+ if (name.equals("pointerIds")) {
+ pointerPropertiesArray = readPointerProperties(jsonReader);
+ } else if (name.equals("xyt")) {
+ readPointerData(jsonReader, replayData, actionType, pointerPropertiesArray);
+ }
+ }
+ jsonReader.endObject();
+ }
+
+ private PointerProperties[] readPointerProperties(final JsonReader jsonReader)
+ throws IOException {
+ final ArrayList<PointerProperties> pointerPropertiesArrayList =
+ new ArrayList<PointerProperties>();
+ jsonReader.beginArray();
+ while (jsonReader.hasNext()) {
+ final PointerProperties pointerProperties = new PointerProperties();
+ pointerProperties.id = jsonReader.nextInt();
+ pointerProperties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
+ pointerPropertiesArrayList.add(pointerProperties);
+ }
+ jsonReader.endArray();
+ return pointerPropertiesArrayList.toArray(
+ new PointerProperties[pointerPropertiesArrayList.size()]);
+ }
+
+ private void readPointerData(final JsonReader jsonReader, final ReplayData replayData,
+ final int actionType, final PointerProperties[] pointerPropertiesArray)
+ throws IOException {
+ if (pointerPropertiesArray == null) {
+ Log.e(TAG, "PointerIDs must be given before xyt data in json for MotionEvent");
+ jsonReader.skipValue();
+ return;
+ }
+ long time = UNINITIALIZED_LONG;
+ jsonReader.beginArray();
+ while (jsonReader.hasNext()) { // Array of historical data
+ jsonReader.beginObject();
+ final ArrayList<PointerCoords> pointerCoordsArrayList = new ArrayList<PointerCoords>();
+ while (jsonReader.hasNext()) { // Time/data object
+ final String name = jsonReader.nextName();
+ if (name.equals("t")) {
+ time = jsonReader.nextLong();
+ } else if (name.equals("d")) {
+ jsonReader.beginArray();
+ while (jsonReader.hasNext()) { // array of data per pointer
+ final PointerCoords pointerCoords = readPointerCoords(jsonReader);
+ if (pointerCoords != null) {
+ pointerCoordsArrayList.add(pointerCoords);
+ }
+ }
+ jsonReader.endArray();
+ } else {
+ jsonReader.skipValue();
+ }
+ }
+ jsonReader.endObject();
+ // Data was recorded as historical events, but must be split apart into
+ // separate MotionEvents for replaying
+ if (time != UNINITIALIZED_LONG) {
+ addMotionEventData(replayData, actionType, time, pointerPropertiesArray,
+ pointerCoordsArrayList.toArray(
+ new PointerCoords[pointerCoordsArrayList.size()]));
+ } else {
+ Log.e(TAG, "Time not assigned in json for MotionEvent");
+ }
+ }
+ jsonReader.endArray();
+ }
+
+ private PointerCoords readPointerCoords(final JsonReader jsonReader) throws IOException {
+ jsonReader.beginObject();
+ float x = UNINITIALIZED_FLOAT;
+ float y = UNINITIALIZED_FLOAT;
+ while (jsonReader.hasNext()) { // x,y
+ final String name = jsonReader.nextName();
+ if (name.equals("x")) {
+ x = (float) jsonReader.nextDouble();
+ } else if (name.equals("y")) {
+ y = (float) jsonReader.nextDouble();
+ } else {
+ jsonReader.skipValue();
+ }
+ }
+ jsonReader.endObject();
+
+ if (Float.compare(x, UNINITIALIZED_FLOAT) == 0
+ || Float.compare(y, UNINITIALIZED_FLOAT) == 0) {
+ Log.w(TAG, "missing x or y value in MotionEvent json");
+ return null;
}
+ final PointerCoords pointerCoords = new PointerCoords();
+ pointerCoords.x = x;
+ pointerCoords.y = y;
+ pointerCoords.pressure = 1.0f;
+ pointerCoords.size = 1.0f;
+ return pointerCoords;
+ }
+
+ /**
+ * Tests that {@code x} is uninitialized.
+ *
+ * Assumes that {@code x} will never be given a valid value less than 0, and that
+ * UNINITIALIZED_FLOAT is less than 0.0f.
+ */
+ private boolean isUninitializedFloat(final float x) {
+ return x < 0.0f;
}
+ private void addMotionEventData(final ReplayData replayData, final int actionType,
+ final long time, final PointerProperties[] pointerProperties,
+ final PointerCoords[] pointerCoords) {
+ replayData.mActions.add(actionType);
+ replayData.mTimes.add(time);
+ replayData.mPointerPropertiesArrays.add(pointerProperties);
+ replayData.mPointerCoordsArrays.add(pointerCoords);
+ }
}
diff --git a/java/src/com/android/inputmethod/research/Replayer.java b/java/src/com/android/inputmethod/research/Replayer.java
index 9bf5fee6c..a9b7a9d0c 100644
--- a/java/src/com/android/inputmethod/research/Replayer.java
+++ b/java/src/com/android/inputmethod/research/Replayer.java
@@ -22,6 +22,8 @@ import android.os.Message;
import android.os.SystemClock;
import android.util.Log;
import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
import com.android.inputmethod.keyboard.KeyboardSwitcher;
import com.android.inputmethod.keyboard.MainKeyboardView;
@@ -62,7 +64,6 @@ public class Replayer {
if (mIsReplaying) {
return;
}
-
mIsReplaying = true;
final int numActions = replayData.mActions.size();
if (DEBUG) {
@@ -95,25 +96,36 @@ public class Replayer {
case MSG_MOTION_EVENT:
final int index = msg.arg1;
final int action = replayData.mActions.get(index);
- final int x = replayData.mXCoords.get(index);
- final int y = replayData.mYCoords.get(index);
+ final PointerProperties[] pointerPropertiesArray =
+ replayData.mPointerPropertiesArrays.get(index);
+ final PointerCoords[] pointerCoordsArray =
+ replayData.mPointerCoordsArrays.get(index);
final long origTime = replayData.mTimes.get(index);
if (action == MotionEvent.ACTION_DOWN) {
mOrigDownTime = origTime;
}
final MotionEvent me = MotionEvent.obtain(mOrigDownTime + timeAdjustment,
- origTime + timeAdjustment, action, x, y, 0);
+ origTime + timeAdjustment, action,
+ pointerPropertiesArray.length, pointerPropertiesArray,
+ pointerCoordsArray, 0, 0, 1.0f, 1.0f, 0, 0, 0, 0);
mainKeyboardView.processMotionEvent(me);
me.recycle();
break;
case MSG_DONE:
mIsReplaying = false;
+ ResearchLogger.getInstance().requestIndicatorRedraw();
break;
}
}
};
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ ResearchLogger.getInstance().requestIndicatorRedraw();
+ }
+ });
for (int i = 0; i < numActions; i++) {
final Message msg = Message.obtain(handler, MSG_MOTION_EVENT, i, 0);
final long msgTime = replayData.mTimes.get(i) + timeAdjustment;
diff --git a/tests/src/com/android/inputmethod/research/MotionEventReaderTests.java b/tests/src/com/android/inputmethod/research/MotionEventReaderTests.java
new file mode 100644
index 000000000..c0eaaead4
--- /dev/null
+++ b/tests/src/com/android/inputmethod/research/MotionEventReaderTests.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2013 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.inputmethod.research;
+
+import android.test.AndroidTestCase;
+import android.util.JsonReader;
+
+import com.android.inputmethod.research.MotionEventReader.ReplayData;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+public class MotionEventReaderTests extends AndroidTestCase {
+ private MotionEventReader mMotionEventReader = new MotionEventReader();
+ private ReplayData mReplayData;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mReplayData = new ReplayData();
+ }
+
+ private JsonReader jsonReaderForString(final String s) {
+ return new JsonReader(new StringReader(s));
+ }
+
+ public void testTopLevelDataVariant() {
+ final JsonReader jsonReader = jsonReaderForString(
+ "{"
+ + "\"_ct\": 1359590400000,"
+ + "\"_ut\": 4381933,"
+ + "\"_ty\": \"MotionEvent\","
+ + "\"action\": \"UP\","
+ + "\"isLoggingRelated\": false,"
+ + "\"x\": 100.0,"
+ + "\"y\": 200.0"
+ + "}"
+ );
+ try {
+ mMotionEventReader.readLogStatement(jsonReader, mReplayData);
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail("IOException thrown");
+ }
+ assertEquals("x set correctly", (int) mReplayData.mPointerCoordsArrays.get(0)[0].x, 100);
+ assertEquals("y set correctly", (int) mReplayData.mPointerCoordsArrays.get(0)[0].y, 200);
+ assertEquals("only one pointer", mReplayData.mPointerCoordsArrays.get(0).length, 1);
+ assertEquals("only one MotionEvent", mReplayData.mPointerCoordsArrays.size(), 1);
+ }
+
+ public void testNestedDataVariant() {
+ final JsonReader jsonReader = jsonReaderForString(
+ "{"
+ + " \"_ct\": 135959040000,"
+ + " \"_ut\": 4382702,"
+ + " \"_ty\": \"MotionEvent\","
+ + " \"action\": \"MOVE\","
+ + " \"isLoggingRelated\": false,"
+ + " \"motionEvent\": {"
+ + " \"pointerIds\": ["
+ + " 0"
+ + " ],"
+ + " \"xyt\": ["
+ + " {"
+ + " \"t\": 4382551,"
+ + " \"d\": ["
+ + " {"
+ + " \"x\": 100.0,"
+ + " \"y\": 200.0,"
+ + " \"toma\": 999.0,"
+ + " \"tomi\": 999.0,"
+ + " \"o\": 0.0"
+ + " }"
+ + " ]"
+ + " },"
+ + " {"
+ + " \"t\": 4382559,"
+ + " \"d\": ["
+ + " {"
+ + " \"x\": 300.0,"
+ + " \"y\": 400.0,"
+ + " \"toma\": 999.0,"
+ + " \"tomi\": 999.0,"
+ + " \"o\": 0.0"
+ + " }"
+ + " ]"
+ + " }"
+ + " ]"
+ + " }"
+ + "}"
+ );
+ try {
+ mMotionEventReader.readLogStatement(jsonReader, mReplayData);
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail("IOException thrown");
+ }
+ assertEquals("x1 set correctly", (int) mReplayData.mPointerCoordsArrays.get(0)[0].x, 100);
+ assertEquals("y1 set correctly", (int) mReplayData.mPointerCoordsArrays.get(0)[0].y, 200);
+ assertEquals("x2 set correctly", (int) mReplayData.mPointerCoordsArrays.get(1)[0].x, 300);
+ assertEquals("y2 set correctly", (int) mReplayData.mPointerCoordsArrays.get(1)[0].y, 400);
+ assertEquals("only one pointer", mReplayData.mPointerCoordsArrays.get(0).length, 1);
+ assertEquals("two MotionEvents", mReplayData.mPointerCoordsArrays.size(), 2);
+ }
+
+ public void testNestedDataVariantMultiPointer() {
+ final JsonReader jsonReader = jsonReaderForString(
+ "{"
+ + " \"_ct\": 135959040000,"
+ + " \"_ut\": 4382702,"
+ + " \"_ty\": \"MotionEvent\","
+ + " \"action\": \"MOVE\","
+ + " \"isLoggingRelated\": false,"
+ + " \"motionEvent\": {"
+ + " \"pointerIds\": ["
+ + " 1"
+ + " ],"
+ + " \"xyt\": ["
+ + " {"
+ + " \"t\": 4382551,"
+ + " \"d\": ["
+ + " {"
+ + " \"x\": 100.0,"
+ + " \"y\": 200.0,"
+ + " \"toma\": 999.0,"
+ + " \"tomi\": 999.0,"
+ + " \"o\": 0.0"
+ + " },"
+ + " {"
+ + " \"x\": 300.0,"
+ + " \"y\": 400.0,"
+ + " \"toma\": 999.0,"
+ + " \"tomi\": 999.0,"
+ + " \"o\": 0.0"
+ + " }"
+ + " ]"
+ + " }"
+ + " ]"
+ + " }"
+ + "}"
+ );
+ try {
+ mMotionEventReader.readLogStatement(jsonReader, mReplayData);
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail("IOException thrown");
+ }
+ assertEquals("x1 set correctly", (int) mReplayData.mPointerCoordsArrays.get(0)[0].x, 100);
+ assertEquals("y1 set correctly", (int) mReplayData.mPointerCoordsArrays.get(0)[0].y, 200);
+ assertEquals("x2 set correctly", (int) mReplayData.mPointerCoordsArrays.get(0)[1].x, 300);
+ assertEquals("y2 set correctly", (int) mReplayData.mPointerCoordsArrays.get(0)[1].y, 400);
+ assertEquals("two pointers", mReplayData.mPointerCoordsArrays.get(0).length, 2);
+ assertEquals("one MotionEvent", mReplayData.mPointerCoordsArrays.size(), 1);
+ }
+}