1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/tests/MotionEventReplayer.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,220 @@ 1.4 +package org.mozilla.gecko.tests; 1.5 + 1.6 +import java.io.BufferedReader; 1.7 +import java.io.IOException; 1.8 +import java.io.InputStream; 1.9 +import java.io.InputStreamReader; 1.10 +import java.lang.reflect.InvocationTargetException; 1.11 +import java.lang.reflect.Method; 1.12 +import java.util.HashMap; 1.13 +import java.util.Map; 1.14 +import java.util.StringTokenizer; 1.15 +import java.util.regex.Matcher; 1.16 +import java.util.regex.Pattern; 1.17 + 1.18 +import android.app.Instrumentation; 1.19 +import android.os.Build; 1.20 +import android.os.SystemClock; 1.21 +import android.util.Log; 1.22 +import android.view.MotionEvent; 1.23 + 1.24 +class MotionEventReplayer { 1.25 + private static final String LOGTAG = "RobocopMotionEventReplayer"; 1.26 + 1.27 + // the inner dimensions of the window on which the motion event capture was taken from 1.28 + private static final int CAPTURE_WINDOW_WIDTH = 720; 1.29 + private static final int CAPTURE_WINDOW_HEIGHT = 1038; 1.30 + 1.31 + private final Instrumentation mInstrumentation; 1.32 + private final int mSurfaceOffsetX; 1.33 + private final int mSurfaceOffsetY; 1.34 + private final int mSurfaceWidth; 1.35 + private final int mSurfaceHeight; 1.36 + private final Map<String, Integer> mActionTypes; 1.37 + private Method mObtainNanoMethod; 1.38 + 1.39 + public MotionEventReplayer(Instrumentation inst, int surfaceOffsetX, int surfaceOffsetY, int surfaceWidth, int surfaceHeight) { 1.40 + mInstrumentation = inst; 1.41 + mSurfaceOffsetX = surfaceOffsetX; 1.42 + mSurfaceOffsetY = surfaceOffsetY; 1.43 + mSurfaceWidth = surfaceWidth; 1.44 + mSurfaceHeight = surfaceHeight; 1.45 + Log.i(LOGTAG, "Initialized using offset (" + mSurfaceOffsetX + "," + mSurfaceOffsetY + ")"); 1.46 + 1.47 + mActionTypes = new HashMap<String, Integer>(); 1.48 + mActionTypes.put("ACTION_CANCEL", MotionEvent.ACTION_CANCEL); 1.49 + mActionTypes.put("ACTION_DOWN", MotionEvent.ACTION_DOWN); 1.50 + mActionTypes.put("ACTION_MOVE", MotionEvent.ACTION_MOVE); 1.51 + mActionTypes.put("ACTION_POINTER_DOWN", MotionEvent.ACTION_POINTER_DOWN); 1.52 + mActionTypes.put("ACTION_POINTER_UP", MotionEvent.ACTION_POINTER_UP); 1.53 + mActionTypes.put("ACTION_UP", MotionEvent.ACTION_UP); 1.54 + } 1.55 + 1.56 + private int parseAction(String action) { 1.57 + int index = 0; 1.58 + 1.59 + // ACTION_POINTER_DOWN and ACTION_POINTER_UP might be followed by 1.60 + // pointer index in parentheses, like ACTION_POINTER_UP(1) 1.61 + int beginParen = action.indexOf("("); 1.62 + if (beginParen >= 0) { 1.63 + int endParen = action.indexOf(")", beginParen + 1); 1.64 + index = Integer.parseInt(action.substring(beginParen + 1, endParen)); 1.65 + action = action.substring(0, beginParen); 1.66 + } 1.67 + 1.68 + return mActionTypes.get(action) | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT); 1.69 + } 1.70 + 1.71 + private int parseInt(String value) { 1.72 + if (value == null) { 1.73 + return 0; 1.74 + } 1.75 + if (value.startsWith("0x")) { 1.76 + return Integer.parseInt(value.substring(2), 16); 1.77 + } 1.78 + return Integer.parseInt(value); 1.79 + } 1.80 + 1.81 + private float scaleX(float value) { 1.82 + return value * (float)mSurfaceWidth / (float)CAPTURE_WINDOW_WIDTH; 1.83 + } 1.84 + 1.85 + private float scaleY(float value) { 1.86 + return value * (float)mSurfaceHeight / (float)CAPTURE_WINDOW_HEIGHT; 1.87 + } 1.88 + 1.89 + public void replayEvents(InputStream eventDescriptions) 1.90 + throws IOException, IllegalAccessException, InvocationTargetException, NoSuchMethodException 1.91 + { 1.92 + // As an example, a line in the input stream might look like: 1.93 + // 1.94 + // MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=424.41055, y[0]=825.2412, 1.95 + // toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, 1.96 + // edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=21972329, 1.97 + // downTime=21972329, deviceId=6, source=0x1002 } 1.98 + // 1.99 + // These can be generated by printing out event.toString() in LayerView's 1.100 + // onTouchEvent function on a phone running Ice Cream Sandwich. Different 1.101 + // Android versions have different serializations of the motion event, and this 1.102 + // code could probably be modified to parse other serializations if needed. 1.103 + Pattern p = Pattern.compile("MotionEvent \\{ (.*?) \\}"); 1.104 + Map<String, String> eventProperties = new HashMap<String, String>(); 1.105 + 1.106 + boolean firstEvent = true; 1.107 + long timeDelta = 0L; 1.108 + long lastEventTime = 0L; 1.109 + 1.110 + BufferedReader br = new BufferedReader(new InputStreamReader(eventDescriptions)); 1.111 + try { 1.112 + for (String eventStr = br.readLine(); eventStr != null; eventStr = br.readLine()) { 1.113 + Matcher m = p.matcher(eventStr); 1.114 + if (! m.find()) { 1.115 + // this line doesn't have any MotionEvent data, skip it 1.116 + continue; 1.117 + } 1.118 + 1.119 + // extract the key-value pairs from the description and store them 1.120 + // in the eventProperties table 1.121 + StringTokenizer keyValues = new StringTokenizer(m.group(1), ","); 1.122 + while (keyValues.hasMoreTokens()) { 1.123 + String keyValue = keyValues.nextToken(); 1.124 + String key = keyValue.substring(0, keyValue.indexOf('=')).trim(); 1.125 + String value = keyValue.substring(keyValue.indexOf('=') + 1).trim(); 1.126 + eventProperties.put(key, value); 1.127 + } 1.128 + 1.129 + // set up the values we need to build the MotionEvent 1.130 + long downTime = Long.parseLong(eventProperties.get("downTime")); 1.131 + long eventTime = Long.parseLong(eventProperties.get("eventTime")); 1.132 + int action = parseAction(eventProperties.get("action")); 1.133 + float pressure = 1.0f; 1.134 + float size = 1.0f; 1.135 + int metaState = parseInt(eventProperties.get("metaState")); 1.136 + float xPrecision = 1.0f; 1.137 + float yPrecision = 1.0f; 1.138 + int deviceId = 0; 1.139 + int edgeFlags = parseInt(eventProperties.get("edgeFlags")); 1.140 + int source = parseInt(eventProperties.get("source")); 1.141 + int flags = parseInt(eventProperties.get("flags")); 1.142 + 1.143 + int pointerCount = parseInt(eventProperties.get("pointerCount")); 1.144 + int[] pointerIds = new int[pointerCount]; 1.145 + Object pointerData; 1.146 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { 1.147 + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount]; 1.148 + for (int i = 0; i < pointerCount; i++) { 1.149 + pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]")); 1.150 + pointerCoords[i] = new MotionEvent.PointerCoords(); 1.151 + pointerCoords[i].x = mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]"))); 1.152 + pointerCoords[i].y = mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]"))); 1.153 + } 1.154 + pointerData = pointerCoords; 1.155 + } else { 1.156 + // pre-gingerbread we have to use a hidden API to create the motion event, and we have 1.157 + // to create a flattened list of floats rather than an array of PointerCoords 1.158 + final int NUM_SAMPLE_DATA = 4; // MotionEvent.NUM_SAMPLE_DATA 1.159 + final int SAMPLE_X = 0; // MotionEvent.SAMPLE_X 1.160 + final int SAMPLE_Y = 1; // MotionEvent.SAMPLE_Y 1.161 + float[] sampleData = new float[pointerCount * NUM_SAMPLE_DATA]; 1.162 + for (int i = 0; i < pointerCount; i++) { 1.163 + pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]")); 1.164 + sampleData[(i * NUM_SAMPLE_DATA) + SAMPLE_X] = 1.165 + mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]"))); 1.166 + sampleData[(i * NUM_SAMPLE_DATA) + SAMPLE_Y] = 1.167 + mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]"))); 1.168 + } 1.169 + pointerData = sampleData; 1.170 + } 1.171 + 1.172 + // we want to adjust the timestamps on all the generated events so that they line up with 1.173 + // the time that this function is executing on-device. 1.174 + long now = SystemClock.uptimeMillis(); 1.175 + if (firstEvent) { 1.176 + timeDelta = now - eventTime; 1.177 + firstEvent = false; 1.178 + } 1.179 + downTime += timeDelta; 1.180 + eventTime += timeDelta; 1.181 + 1.182 + // we also generate the events in "real-time" (i.e. have delays between events that 1.183 + // correspond to the delays in the event timestamps). 1.184 + if (now < eventTime) { 1.185 + try { 1.186 + Thread.sleep(eventTime - now); 1.187 + } catch (InterruptedException ie) { 1.188 + } 1.189 + } 1.190 + 1.191 + // and finally we dispatch the event 1.192 + MotionEvent event; 1.193 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { 1.194 + event = MotionEvent.obtain(downTime, eventTime, action, pointerCount, 1.195 + pointerIds, (MotionEvent.PointerCoords[])pointerData, metaState, 1.196 + xPrecision, yPrecision, deviceId, edgeFlags, source, flags); 1.197 + } else { 1.198 + // pre-gingerbread we have to use a hidden API to accomplish this 1.199 + if (mObtainNanoMethod == null) { 1.200 + mObtainNanoMethod = MotionEvent.class.getMethod("obtainNano", long.class, 1.201 + long.class, long.class, int.class, int.class, pointerIds.getClass(), 1.202 + pointerData.getClass(), int.class, float.class, float.class, 1.203 + int.class, int.class); 1.204 + } 1.205 + event = (MotionEvent)mObtainNanoMethod.invoke(null, downTime, eventTime, 1.206 + eventTime * 1000000, action, pointerCount, pointerIds, (float[])pointerData, 1.207 + metaState, xPrecision, yPrecision, deviceId, edgeFlags); 1.208 + } 1.209 + try { 1.210 + Log.v(LOGTAG, "Injecting " + event.toString()); 1.211 + mInstrumentation.sendPointerSync(event); 1.212 + } finally { 1.213 + event.recycle(); 1.214 + event = null; 1.215 + } 1.216 + 1.217 + eventProperties.clear(); 1.218 + } 1.219 + } finally { 1.220 + br.close(); 1.221 + } 1.222 + } 1.223 +}