|
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 } |