diff -r 000000000000 -r 6474c204b198 mobile/android/base/tests/MotionEventReplayer.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/base/tests/MotionEventReplayer.java Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,220 @@ +package org.mozilla.gecko.tests; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +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"; + + // the inner dimensions of the window on which the motion event capture was taken from + private static final int CAPTURE_WINDOW_WIDTH = 720; + private static final int CAPTURE_WINDOW_HEIGHT = 1038; + + private final Instrumentation mInstrumentation; + private final int mSurfaceOffsetX; + private final int mSurfaceOffsetY; + private final int mSurfaceWidth; + private final int mSurfaceHeight; + private final Map mActionTypes; + private Method mObtainNanoMethod; + + public MotionEventReplayer(Instrumentation inst, int surfaceOffsetX, int surfaceOffsetY, int surfaceWidth, int surfaceHeight) { + mInstrumentation = inst; + mSurfaceOffsetX = surfaceOffsetX; + mSurfaceOffsetY = surfaceOffsetY; + mSurfaceWidth = surfaceWidth; + mSurfaceHeight = surfaceHeight; + 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); + } + + private float scaleX(float value) { + return value * (float)mSurfaceWidth / (float)CAPTURE_WINDOW_WIDTH; + } + + private float scaleY(float value) { + return value * (float)mSurfaceHeight / (float)CAPTURE_WINDOW_HEIGHT; + } + + 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 LayerView'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 + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]"))); + pointerCoords[i].y = mSurfaceOffsetY + scaleY(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 + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]"))); + sampleData[(i * NUM_SAMPLE_DATA) + SAMPLE_Y] = + mSurfaceOffsetY + scaleY(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); + } + try { + Log.v(LOGTAG, "Injecting " + event.toString()); + mInstrumentation.sendPointerSync(event); + } finally { + event.recycle(); + event = null; + } + + eventProperties.clear(); + } + } finally { + br.close(); + } + } +}