mobile/android/base/tests/MotionEventReplayer.java

changeset 0
6474c204b198
     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 +}

mercurial