mobile/android/base/tests/MotionEventReplayer.java

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:5626f71cf174
1 package org.mozilla.gecko.tests;
2
3 import java.io.BufferedReader;
4 import java.io.IOException;
5 import java.io.InputStream;
6 import java.io.InputStreamReader;
7 import java.lang.reflect.InvocationTargetException;
8 import java.lang.reflect.Method;
9 import java.util.HashMap;
10 import java.util.Map;
11 import java.util.StringTokenizer;
12 import java.util.regex.Matcher;
13 import java.util.regex.Pattern;
14
15 import android.app.Instrumentation;
16 import android.os.Build;
17 import android.os.SystemClock;
18 import android.util.Log;
19 import android.view.MotionEvent;
20
21 class MotionEventReplayer {
22 private static final String LOGTAG = "RobocopMotionEventReplayer";
23
24 // the inner dimensions of the window on which the motion event capture was taken from
25 private static final int CAPTURE_WINDOW_WIDTH = 720;
26 private static final int CAPTURE_WINDOW_HEIGHT = 1038;
27
28 private final Instrumentation mInstrumentation;
29 private final int mSurfaceOffsetX;
30 private final int mSurfaceOffsetY;
31 private final int mSurfaceWidth;
32 private final int mSurfaceHeight;
33 private final Map<String, Integer> mActionTypes;
34 private Method mObtainNanoMethod;
35
36 public MotionEventReplayer(Instrumentation inst, int surfaceOffsetX, int surfaceOffsetY, int surfaceWidth, int surfaceHeight) {
37 mInstrumentation = inst;
38 mSurfaceOffsetX = surfaceOffsetX;
39 mSurfaceOffsetY = surfaceOffsetY;
40 mSurfaceWidth = surfaceWidth;
41 mSurfaceHeight = surfaceHeight;
42 Log.i(LOGTAG, "Initialized using offset (" + mSurfaceOffsetX + "," + mSurfaceOffsetY + ")");
43
44 mActionTypes = new HashMap<String, Integer>();
45 mActionTypes.put("ACTION_CANCEL", MotionEvent.ACTION_CANCEL);
46 mActionTypes.put("ACTION_DOWN", MotionEvent.ACTION_DOWN);
47 mActionTypes.put("ACTION_MOVE", MotionEvent.ACTION_MOVE);
48 mActionTypes.put("ACTION_POINTER_DOWN", MotionEvent.ACTION_POINTER_DOWN);
49 mActionTypes.put("ACTION_POINTER_UP", MotionEvent.ACTION_POINTER_UP);
50 mActionTypes.put("ACTION_UP", MotionEvent.ACTION_UP);
51 }
52
53 private int parseAction(String action) {
54 int index = 0;
55
56 // ACTION_POINTER_DOWN and ACTION_POINTER_UP might be followed by
57 // pointer index in parentheses, like ACTION_POINTER_UP(1)
58 int beginParen = action.indexOf("(");
59 if (beginParen >= 0) {
60 int endParen = action.indexOf(")", beginParen + 1);
61 index = Integer.parseInt(action.substring(beginParen + 1, endParen));
62 action = action.substring(0, beginParen);
63 }
64
65 return mActionTypes.get(action) | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
66 }
67
68 private int parseInt(String value) {
69 if (value == null) {
70 return 0;
71 }
72 if (value.startsWith("0x")) {
73 return Integer.parseInt(value.substring(2), 16);
74 }
75 return Integer.parseInt(value);
76 }
77
78 private float scaleX(float value) {
79 return value * (float)mSurfaceWidth / (float)CAPTURE_WINDOW_WIDTH;
80 }
81
82 private float scaleY(float value) {
83 return value * (float)mSurfaceHeight / (float)CAPTURE_WINDOW_HEIGHT;
84 }
85
86 public void replayEvents(InputStream eventDescriptions)
87 throws IOException, IllegalAccessException, InvocationTargetException, NoSuchMethodException
88 {
89 // As an example, a line in the input stream might look like:
90 //
91 // MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=424.41055, y[0]=825.2412,
92 // toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0,
93 // edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=21972329,
94 // downTime=21972329, deviceId=6, source=0x1002 }
95 //
96 // These can be generated by printing out event.toString() in LayerView's
97 // onTouchEvent function on a phone running Ice Cream Sandwich. Different
98 // Android versions have different serializations of the motion event, and this
99 // code could probably be modified to parse other serializations if needed.
100 Pattern p = Pattern.compile("MotionEvent \\{ (.*?) \\}");
101 Map<String, String> eventProperties = new HashMap<String, String>();
102
103 boolean firstEvent = true;
104 long timeDelta = 0L;
105 long lastEventTime = 0L;
106
107 BufferedReader br = new BufferedReader(new InputStreamReader(eventDescriptions));
108 try {
109 for (String eventStr = br.readLine(); eventStr != null; eventStr = br.readLine()) {
110 Matcher m = p.matcher(eventStr);
111 if (! m.find()) {
112 // this line doesn't have any MotionEvent data, skip it
113 continue;
114 }
115
116 // extract the key-value pairs from the description and store them
117 // in the eventProperties table
118 StringTokenizer keyValues = new StringTokenizer(m.group(1), ",");
119 while (keyValues.hasMoreTokens()) {
120 String keyValue = keyValues.nextToken();
121 String key = keyValue.substring(0, keyValue.indexOf('=')).trim();
122 String value = keyValue.substring(keyValue.indexOf('=') + 1).trim();
123 eventProperties.put(key, value);
124 }
125
126 // set up the values we need to build the MotionEvent
127 long downTime = Long.parseLong(eventProperties.get("downTime"));
128 long eventTime = Long.parseLong(eventProperties.get("eventTime"));
129 int action = parseAction(eventProperties.get("action"));
130 float pressure = 1.0f;
131 float size = 1.0f;
132 int metaState = parseInt(eventProperties.get("metaState"));
133 float xPrecision = 1.0f;
134 float yPrecision = 1.0f;
135 int deviceId = 0;
136 int edgeFlags = parseInt(eventProperties.get("edgeFlags"));
137 int source = parseInt(eventProperties.get("source"));
138 int flags = parseInt(eventProperties.get("flags"));
139
140 int pointerCount = parseInt(eventProperties.get("pointerCount"));
141 int[] pointerIds = new int[pointerCount];
142 Object pointerData;
143 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
144 MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount];
145 for (int i = 0; i < pointerCount; i++) {
146 pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]"));
147 pointerCoords[i] = new MotionEvent.PointerCoords();
148 pointerCoords[i].x = mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]")));
149 pointerCoords[i].y = mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]")));
150 }
151 pointerData = pointerCoords;
152 } else {
153 // pre-gingerbread we have to use a hidden API to create the motion event, and we have
154 // to create a flattened list of floats rather than an array of PointerCoords
155 final int NUM_SAMPLE_DATA = 4; // MotionEvent.NUM_SAMPLE_DATA
156 final int SAMPLE_X = 0; // MotionEvent.SAMPLE_X
157 final int SAMPLE_Y = 1; // MotionEvent.SAMPLE_Y
158 float[] sampleData = new float[pointerCount * NUM_SAMPLE_DATA];
159 for (int i = 0; i < pointerCount; i++) {
160 pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]"));
161 sampleData[(i * NUM_SAMPLE_DATA) + SAMPLE_X] =
162 mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]")));
163 sampleData[(i * NUM_SAMPLE_DATA) + SAMPLE_Y] =
164 mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]")));
165 }
166 pointerData = sampleData;
167 }
168
169 // we want to adjust the timestamps on all the generated events so that they line up with
170 // the time that this function is executing on-device.
171 long now = SystemClock.uptimeMillis();
172 if (firstEvent) {
173 timeDelta = now - eventTime;
174 firstEvent = false;
175 }
176 downTime += timeDelta;
177 eventTime += timeDelta;
178
179 // we also generate the events in "real-time" (i.e. have delays between events that
180 // correspond to the delays in the event timestamps).
181 if (now < eventTime) {
182 try {
183 Thread.sleep(eventTime - now);
184 } catch (InterruptedException ie) {
185 }
186 }
187
188 // and finally we dispatch the event
189 MotionEvent event;
190 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
191 event = MotionEvent.obtain(downTime, eventTime, action, pointerCount,
192 pointerIds, (MotionEvent.PointerCoords[])pointerData, metaState,
193 xPrecision, yPrecision, deviceId, edgeFlags, source, flags);
194 } else {
195 // pre-gingerbread we have to use a hidden API to accomplish this
196 if (mObtainNanoMethod == null) {
197 mObtainNanoMethod = MotionEvent.class.getMethod("obtainNano", long.class,
198 long.class, long.class, int.class, int.class, pointerIds.getClass(),
199 pointerData.getClass(), int.class, float.class, float.class,
200 int.class, int.class);
201 }
202 event = (MotionEvent)mObtainNanoMethod.invoke(null, downTime, eventTime,
203 eventTime * 1000000, action, pointerCount, pointerIds, (float[])pointerData,
204 metaState, xPrecision, yPrecision, deviceId, edgeFlags);
205 }
206 try {
207 Log.v(LOGTAG, "Injecting " + event.toString());
208 mInstrumentation.sendPointerSync(event);
209 } finally {
210 event.recycle();
211 event = null;
212 }
213
214 eventProperties.clear();
215 }
216 } finally {
217 br.close();
218 }
219 }
220 }

mercurial