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