mobile/android/base/tests/MotionEventReplayer.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

michael@0 1 package org.mozilla.gecko.tests;
michael@0 2
michael@0 3 import java.io.BufferedReader;
michael@0 4 import java.io.IOException;
michael@0 5 import java.io.InputStream;
michael@0 6 import java.io.InputStreamReader;
michael@0 7 import java.lang.reflect.InvocationTargetException;
michael@0 8 import java.lang.reflect.Method;
michael@0 9 import java.util.HashMap;
michael@0 10 import java.util.Map;
michael@0 11 import java.util.StringTokenizer;
michael@0 12 import java.util.regex.Matcher;
michael@0 13 import java.util.regex.Pattern;
michael@0 14
michael@0 15 import android.app.Instrumentation;
michael@0 16 import android.os.Build;
michael@0 17 import android.os.SystemClock;
michael@0 18 import android.util.Log;
michael@0 19 import android.view.MotionEvent;
michael@0 20
michael@0 21 class MotionEventReplayer {
michael@0 22 private static final String LOGTAG = "RobocopMotionEventReplayer";
michael@0 23
michael@0 24 // the inner dimensions of the window on which the motion event capture was taken from
michael@0 25 private static final int CAPTURE_WINDOW_WIDTH = 720;
michael@0 26 private static final int CAPTURE_WINDOW_HEIGHT = 1038;
michael@0 27
michael@0 28 private final Instrumentation mInstrumentation;
michael@0 29 private final int mSurfaceOffsetX;
michael@0 30 private final int mSurfaceOffsetY;
michael@0 31 private final int mSurfaceWidth;
michael@0 32 private final int mSurfaceHeight;
michael@0 33 private final Map<String, Integer> mActionTypes;
michael@0 34 private Method mObtainNanoMethod;
michael@0 35
michael@0 36 public MotionEventReplayer(Instrumentation inst, int surfaceOffsetX, int surfaceOffsetY, int surfaceWidth, int surfaceHeight) {
michael@0 37 mInstrumentation = inst;
michael@0 38 mSurfaceOffsetX = surfaceOffsetX;
michael@0 39 mSurfaceOffsetY = surfaceOffsetY;
michael@0 40 mSurfaceWidth = surfaceWidth;
michael@0 41 mSurfaceHeight = surfaceHeight;
michael@0 42 Log.i(LOGTAG, "Initialized using offset (" + mSurfaceOffsetX + "," + mSurfaceOffsetY + ")");
michael@0 43
michael@0 44 mActionTypes = new HashMap<String, Integer>();
michael@0 45 mActionTypes.put("ACTION_CANCEL", MotionEvent.ACTION_CANCEL);
michael@0 46 mActionTypes.put("ACTION_DOWN", MotionEvent.ACTION_DOWN);
michael@0 47 mActionTypes.put("ACTION_MOVE", MotionEvent.ACTION_MOVE);
michael@0 48 mActionTypes.put("ACTION_POINTER_DOWN", MotionEvent.ACTION_POINTER_DOWN);
michael@0 49 mActionTypes.put("ACTION_POINTER_UP", MotionEvent.ACTION_POINTER_UP);
michael@0 50 mActionTypes.put("ACTION_UP", MotionEvent.ACTION_UP);
michael@0 51 }
michael@0 52
michael@0 53 private int parseAction(String action) {
michael@0 54 int index = 0;
michael@0 55
michael@0 56 // ACTION_POINTER_DOWN and ACTION_POINTER_UP might be followed by
michael@0 57 // pointer index in parentheses, like ACTION_POINTER_UP(1)
michael@0 58 int beginParen = action.indexOf("(");
michael@0 59 if (beginParen >= 0) {
michael@0 60 int endParen = action.indexOf(")", beginParen + 1);
michael@0 61 index = Integer.parseInt(action.substring(beginParen + 1, endParen));
michael@0 62 action = action.substring(0, beginParen);
michael@0 63 }
michael@0 64
michael@0 65 return mActionTypes.get(action) | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
michael@0 66 }
michael@0 67
michael@0 68 private int parseInt(String value) {
michael@0 69 if (value == null) {
michael@0 70 return 0;
michael@0 71 }
michael@0 72 if (value.startsWith("0x")) {
michael@0 73 return Integer.parseInt(value.substring(2), 16);
michael@0 74 }
michael@0 75 return Integer.parseInt(value);
michael@0 76 }
michael@0 77
michael@0 78 private float scaleX(float value) {
michael@0 79 return value * (float)mSurfaceWidth / (float)CAPTURE_WINDOW_WIDTH;
michael@0 80 }
michael@0 81
michael@0 82 private float scaleY(float value) {
michael@0 83 return value * (float)mSurfaceHeight / (float)CAPTURE_WINDOW_HEIGHT;
michael@0 84 }
michael@0 85
michael@0 86 public void replayEvents(InputStream eventDescriptions)
michael@0 87 throws IOException, IllegalAccessException, InvocationTargetException, NoSuchMethodException
michael@0 88 {
michael@0 89 // As an example, a line in the input stream might look like:
michael@0 90 //
michael@0 91 // MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=424.41055, y[0]=825.2412,
michael@0 92 // toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0,
michael@0 93 // edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=21972329,
michael@0 94 // downTime=21972329, deviceId=6, source=0x1002 }
michael@0 95 //
michael@0 96 // These can be generated by printing out event.toString() in LayerView's
michael@0 97 // onTouchEvent function on a phone running Ice Cream Sandwich. Different
michael@0 98 // Android versions have different serializations of the motion event, and this
michael@0 99 // code could probably be modified to parse other serializations if needed.
michael@0 100 Pattern p = Pattern.compile("MotionEvent \\{ (.*?) \\}");
michael@0 101 Map<String, String> eventProperties = new HashMap<String, String>();
michael@0 102
michael@0 103 boolean firstEvent = true;
michael@0 104 long timeDelta = 0L;
michael@0 105 long lastEventTime = 0L;
michael@0 106
michael@0 107 BufferedReader br = new BufferedReader(new InputStreamReader(eventDescriptions));
michael@0 108 try {
michael@0 109 for (String eventStr = br.readLine(); eventStr != null; eventStr = br.readLine()) {
michael@0 110 Matcher m = p.matcher(eventStr);
michael@0 111 if (! m.find()) {
michael@0 112 // this line doesn't have any MotionEvent data, skip it
michael@0 113 continue;
michael@0 114 }
michael@0 115
michael@0 116 // extract the key-value pairs from the description and store them
michael@0 117 // in the eventProperties table
michael@0 118 StringTokenizer keyValues = new StringTokenizer(m.group(1), ",");
michael@0 119 while (keyValues.hasMoreTokens()) {
michael@0 120 String keyValue = keyValues.nextToken();
michael@0 121 String key = keyValue.substring(0, keyValue.indexOf('=')).trim();
michael@0 122 String value = keyValue.substring(keyValue.indexOf('=') + 1).trim();
michael@0 123 eventProperties.put(key, value);
michael@0 124 }
michael@0 125
michael@0 126 // set up the values we need to build the MotionEvent
michael@0 127 long downTime = Long.parseLong(eventProperties.get("downTime"));
michael@0 128 long eventTime = Long.parseLong(eventProperties.get("eventTime"));
michael@0 129 int action = parseAction(eventProperties.get("action"));
michael@0 130 float pressure = 1.0f;
michael@0 131 float size = 1.0f;
michael@0 132 int metaState = parseInt(eventProperties.get("metaState"));
michael@0 133 float xPrecision = 1.0f;
michael@0 134 float yPrecision = 1.0f;
michael@0 135 int deviceId = 0;
michael@0 136 int edgeFlags = parseInt(eventProperties.get("edgeFlags"));
michael@0 137 int source = parseInt(eventProperties.get("source"));
michael@0 138 int flags = parseInt(eventProperties.get("flags"));
michael@0 139
michael@0 140 int pointerCount = parseInt(eventProperties.get("pointerCount"));
michael@0 141 int[] pointerIds = new int[pointerCount];
michael@0 142 Object pointerData;
michael@0 143 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
michael@0 144 MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount];
michael@0 145 for (int i = 0; i < pointerCount; i++) {
michael@0 146 pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]"));
michael@0 147 pointerCoords[i] = new MotionEvent.PointerCoords();
michael@0 148 pointerCoords[i].x = mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]")));
michael@0 149 pointerCoords[i].y = mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]")));
michael@0 150 }
michael@0 151 pointerData = pointerCoords;
michael@0 152 } else {
michael@0 153 // pre-gingerbread we have to use a hidden API to create the motion event, and we have
michael@0 154 // to create a flattened list of floats rather than an array of PointerCoords
michael@0 155 final int NUM_SAMPLE_DATA = 4; // MotionEvent.NUM_SAMPLE_DATA
michael@0 156 final int SAMPLE_X = 0; // MotionEvent.SAMPLE_X
michael@0 157 final int SAMPLE_Y = 1; // MotionEvent.SAMPLE_Y
michael@0 158 float[] sampleData = new float[pointerCount * NUM_SAMPLE_DATA];
michael@0 159 for (int i = 0; i < pointerCount; i++) {
michael@0 160 pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]"));
michael@0 161 sampleData[(i * NUM_SAMPLE_DATA) + SAMPLE_X] =
michael@0 162 mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]")));
michael@0 163 sampleData[(i * NUM_SAMPLE_DATA) + SAMPLE_Y] =
michael@0 164 mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]")));
michael@0 165 }
michael@0 166 pointerData = sampleData;
michael@0 167 }
michael@0 168
michael@0 169 // we want to adjust the timestamps on all the generated events so that they line up with
michael@0 170 // the time that this function is executing on-device.
michael@0 171 long now = SystemClock.uptimeMillis();
michael@0 172 if (firstEvent) {
michael@0 173 timeDelta = now - eventTime;
michael@0 174 firstEvent = false;
michael@0 175 }
michael@0 176 downTime += timeDelta;
michael@0 177 eventTime += timeDelta;
michael@0 178
michael@0 179 // we also generate the events in "real-time" (i.e. have delays between events that
michael@0 180 // correspond to the delays in the event timestamps).
michael@0 181 if (now < eventTime) {
michael@0 182 try {
michael@0 183 Thread.sleep(eventTime - now);
michael@0 184 } catch (InterruptedException ie) {
michael@0 185 }
michael@0 186 }
michael@0 187
michael@0 188 // and finally we dispatch the event
michael@0 189 MotionEvent event;
michael@0 190 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
michael@0 191 event = MotionEvent.obtain(downTime, eventTime, action, pointerCount,
michael@0 192 pointerIds, (MotionEvent.PointerCoords[])pointerData, metaState,
michael@0 193 xPrecision, yPrecision, deviceId, edgeFlags, source, flags);
michael@0 194 } else {
michael@0 195 // pre-gingerbread we have to use a hidden API to accomplish this
michael@0 196 if (mObtainNanoMethod == null) {
michael@0 197 mObtainNanoMethod = MotionEvent.class.getMethod("obtainNano", long.class,
michael@0 198 long.class, long.class, int.class, int.class, pointerIds.getClass(),
michael@0 199 pointerData.getClass(), int.class, float.class, float.class,
michael@0 200 int.class, int.class);
michael@0 201 }
michael@0 202 event = (MotionEvent)mObtainNanoMethod.invoke(null, downTime, eventTime,
michael@0 203 eventTime * 1000000, action, pointerCount, pointerIds, (float[])pointerData,
michael@0 204 metaState, xPrecision, yPrecision, deviceId, edgeFlags);
michael@0 205 }
michael@0 206 try {
michael@0 207 Log.v(LOGTAG, "Injecting " + event.toString());
michael@0 208 mInstrumentation.sendPointerSync(event);
michael@0 209 } finally {
michael@0 210 event.recycle();
michael@0 211 event = null;
michael@0 212 }
michael@0 213
michael@0 214 eventProperties.clear();
michael@0 215 }
michael@0 216 } finally {
michael@0 217 br.close();
michael@0 218 }
michael@0 219 }
michael@0 220 }

mercurial