michael@0: package org.mozilla.gecko.tests; michael@0: michael@0: import java.io.BufferedReader; michael@0: import java.io.IOException; michael@0: import java.io.InputStream; michael@0: import java.io.InputStreamReader; michael@0: import java.lang.reflect.InvocationTargetException; michael@0: import java.lang.reflect.Method; michael@0: import java.util.HashMap; michael@0: import java.util.Map; michael@0: import java.util.StringTokenizer; michael@0: import java.util.regex.Matcher; michael@0: import java.util.regex.Pattern; michael@0: michael@0: import android.app.Instrumentation; michael@0: import android.os.Build; michael@0: import android.os.SystemClock; michael@0: import android.util.Log; michael@0: import android.view.MotionEvent; michael@0: michael@0: class MotionEventReplayer { michael@0: private static final String LOGTAG = "RobocopMotionEventReplayer"; michael@0: michael@0: // the inner dimensions of the window on which the motion event capture was taken from michael@0: private static final int CAPTURE_WINDOW_WIDTH = 720; michael@0: private static final int CAPTURE_WINDOW_HEIGHT = 1038; michael@0: michael@0: private final Instrumentation mInstrumentation; michael@0: private final int mSurfaceOffsetX; michael@0: private final int mSurfaceOffsetY; michael@0: private final int mSurfaceWidth; michael@0: private final int mSurfaceHeight; michael@0: private final Map mActionTypes; michael@0: private Method mObtainNanoMethod; michael@0: michael@0: public MotionEventReplayer(Instrumentation inst, int surfaceOffsetX, int surfaceOffsetY, int surfaceWidth, int surfaceHeight) { michael@0: mInstrumentation = inst; michael@0: mSurfaceOffsetX = surfaceOffsetX; michael@0: mSurfaceOffsetY = surfaceOffsetY; michael@0: mSurfaceWidth = surfaceWidth; michael@0: mSurfaceHeight = surfaceHeight; michael@0: Log.i(LOGTAG, "Initialized using offset (" + mSurfaceOffsetX + "," + mSurfaceOffsetY + ")"); michael@0: michael@0: mActionTypes = new HashMap(); michael@0: mActionTypes.put("ACTION_CANCEL", MotionEvent.ACTION_CANCEL); michael@0: mActionTypes.put("ACTION_DOWN", MotionEvent.ACTION_DOWN); michael@0: mActionTypes.put("ACTION_MOVE", MotionEvent.ACTION_MOVE); michael@0: mActionTypes.put("ACTION_POINTER_DOWN", MotionEvent.ACTION_POINTER_DOWN); michael@0: mActionTypes.put("ACTION_POINTER_UP", MotionEvent.ACTION_POINTER_UP); michael@0: mActionTypes.put("ACTION_UP", MotionEvent.ACTION_UP); michael@0: } michael@0: michael@0: private int parseAction(String action) { michael@0: int index = 0; michael@0: michael@0: // ACTION_POINTER_DOWN and ACTION_POINTER_UP might be followed by michael@0: // pointer index in parentheses, like ACTION_POINTER_UP(1) michael@0: int beginParen = action.indexOf("("); michael@0: if (beginParen >= 0) { michael@0: int endParen = action.indexOf(")", beginParen + 1); michael@0: index = Integer.parseInt(action.substring(beginParen + 1, endParen)); michael@0: action = action.substring(0, beginParen); michael@0: } michael@0: michael@0: return mActionTypes.get(action) | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT); michael@0: } michael@0: michael@0: private int parseInt(String value) { michael@0: if (value == null) { michael@0: return 0; michael@0: } michael@0: if (value.startsWith("0x")) { michael@0: return Integer.parseInt(value.substring(2), 16); michael@0: } michael@0: return Integer.parseInt(value); michael@0: } michael@0: michael@0: private float scaleX(float value) { michael@0: return value * (float)mSurfaceWidth / (float)CAPTURE_WINDOW_WIDTH; michael@0: } michael@0: michael@0: private float scaleY(float value) { michael@0: return value * (float)mSurfaceHeight / (float)CAPTURE_WINDOW_HEIGHT; michael@0: } michael@0: michael@0: public void replayEvents(InputStream eventDescriptions) michael@0: throws IOException, IllegalAccessException, InvocationTargetException, NoSuchMethodException michael@0: { michael@0: // As an example, a line in the input stream might look like: michael@0: // michael@0: // MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=424.41055, y[0]=825.2412, michael@0: // toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, michael@0: // edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=21972329, michael@0: // downTime=21972329, deviceId=6, source=0x1002 } michael@0: // michael@0: // These can be generated by printing out event.toString() in LayerView's michael@0: // onTouchEvent function on a phone running Ice Cream Sandwich. Different michael@0: // Android versions have different serializations of the motion event, and this michael@0: // code could probably be modified to parse other serializations if needed. michael@0: Pattern p = Pattern.compile("MotionEvent \\{ (.*?) \\}"); michael@0: Map eventProperties = new HashMap(); michael@0: michael@0: boolean firstEvent = true; michael@0: long timeDelta = 0L; michael@0: long lastEventTime = 0L; michael@0: michael@0: BufferedReader br = new BufferedReader(new InputStreamReader(eventDescriptions)); michael@0: try { michael@0: for (String eventStr = br.readLine(); eventStr != null; eventStr = br.readLine()) { michael@0: Matcher m = p.matcher(eventStr); michael@0: if (! m.find()) { michael@0: // this line doesn't have any MotionEvent data, skip it michael@0: continue; michael@0: } michael@0: michael@0: // extract the key-value pairs from the description and store them michael@0: // in the eventProperties table michael@0: StringTokenizer keyValues = new StringTokenizer(m.group(1), ","); michael@0: while (keyValues.hasMoreTokens()) { michael@0: String keyValue = keyValues.nextToken(); michael@0: String key = keyValue.substring(0, keyValue.indexOf('=')).trim(); michael@0: String value = keyValue.substring(keyValue.indexOf('=') + 1).trim(); michael@0: eventProperties.put(key, value); michael@0: } michael@0: michael@0: // set up the values we need to build the MotionEvent michael@0: long downTime = Long.parseLong(eventProperties.get("downTime")); michael@0: long eventTime = Long.parseLong(eventProperties.get("eventTime")); michael@0: int action = parseAction(eventProperties.get("action")); michael@0: float pressure = 1.0f; michael@0: float size = 1.0f; michael@0: int metaState = parseInt(eventProperties.get("metaState")); michael@0: float xPrecision = 1.0f; michael@0: float yPrecision = 1.0f; michael@0: int deviceId = 0; michael@0: int edgeFlags = parseInt(eventProperties.get("edgeFlags")); michael@0: int source = parseInt(eventProperties.get("source")); michael@0: int flags = parseInt(eventProperties.get("flags")); michael@0: michael@0: int pointerCount = parseInt(eventProperties.get("pointerCount")); michael@0: int[] pointerIds = new int[pointerCount]; michael@0: Object pointerData; michael@0: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { michael@0: MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount]; michael@0: for (int i = 0; i < pointerCount; i++) { michael@0: pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]")); michael@0: pointerCoords[i] = new MotionEvent.PointerCoords(); michael@0: pointerCoords[i].x = mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]"))); michael@0: pointerCoords[i].y = mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]"))); michael@0: } michael@0: pointerData = pointerCoords; michael@0: } else { michael@0: // pre-gingerbread we have to use a hidden API to create the motion event, and we have michael@0: // to create a flattened list of floats rather than an array of PointerCoords michael@0: final int NUM_SAMPLE_DATA = 4; // MotionEvent.NUM_SAMPLE_DATA michael@0: final int SAMPLE_X = 0; // MotionEvent.SAMPLE_X michael@0: final int SAMPLE_Y = 1; // MotionEvent.SAMPLE_Y michael@0: float[] sampleData = new float[pointerCount * NUM_SAMPLE_DATA]; michael@0: for (int i = 0; i < pointerCount; i++) { michael@0: pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]")); michael@0: sampleData[(i * NUM_SAMPLE_DATA) + SAMPLE_X] = michael@0: mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]"))); michael@0: sampleData[(i * NUM_SAMPLE_DATA) + SAMPLE_Y] = michael@0: mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]"))); michael@0: } michael@0: pointerData = sampleData; michael@0: } michael@0: michael@0: // we want to adjust the timestamps on all the generated events so that they line up with michael@0: // the time that this function is executing on-device. michael@0: long now = SystemClock.uptimeMillis(); michael@0: if (firstEvent) { michael@0: timeDelta = now - eventTime; michael@0: firstEvent = false; michael@0: } michael@0: downTime += timeDelta; michael@0: eventTime += timeDelta; michael@0: michael@0: // we also generate the events in "real-time" (i.e. have delays between events that michael@0: // correspond to the delays in the event timestamps). michael@0: if (now < eventTime) { michael@0: try { michael@0: Thread.sleep(eventTime - now); michael@0: } catch (InterruptedException ie) { michael@0: } michael@0: } michael@0: michael@0: // and finally we dispatch the event michael@0: MotionEvent event; michael@0: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { michael@0: event = MotionEvent.obtain(downTime, eventTime, action, pointerCount, michael@0: pointerIds, (MotionEvent.PointerCoords[])pointerData, metaState, michael@0: xPrecision, yPrecision, deviceId, edgeFlags, source, flags); michael@0: } else { michael@0: // pre-gingerbread we have to use a hidden API to accomplish this michael@0: if (mObtainNanoMethod == null) { michael@0: mObtainNanoMethod = MotionEvent.class.getMethod("obtainNano", long.class, michael@0: long.class, long.class, int.class, int.class, pointerIds.getClass(), michael@0: pointerData.getClass(), int.class, float.class, float.class, michael@0: int.class, int.class); michael@0: } michael@0: event = (MotionEvent)mObtainNanoMethod.invoke(null, downTime, eventTime, michael@0: eventTime * 1000000, action, pointerCount, pointerIds, (float[])pointerData, michael@0: metaState, xPrecision, yPrecision, deviceId, edgeFlags); michael@0: } michael@0: try { michael@0: Log.v(LOGTAG, "Injecting " + event.toString()); michael@0: mInstrumentation.sendPointerSync(event); michael@0: } finally { michael@0: event.recycle(); michael@0: event = null; michael@0: } michael@0: michael@0: eventProperties.clear(); michael@0: } michael@0: } finally { michael@0: br.close(); michael@0: } michael@0: } michael@0: }