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