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