1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.inputmethod.research;
18
19import android.util.JsonReader;
20import android.util.Log;
21import android.view.MotionEvent;
22import android.view.MotionEvent.PointerCoords;
23import android.view.MotionEvent.PointerProperties;
24
25import com.android.inputmethod.annotations.UsedForTesting;
26import com.android.inputmethod.latin.define.ProductionFlag;
27
28import java.io.BufferedReader;
29import java.io.File;
30import java.io.FileInputStream;
31import java.io.FileNotFoundException;
32import java.io.IOException;
33import java.io.InputStreamReader;
34import java.util.ArrayList;
35
36public class MotionEventReader {
37    private static final String TAG = MotionEventReader.class.getSimpleName();
38    private static final boolean DEBUG = false
39            && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
40    // Assumes that MotionEvent.ACTION_MASK does not have all bits set.`
41    private static final int UNINITIALIZED_ACTION = ~MotionEvent.ACTION_MASK;
42    // No legitimate int is negative
43    private static final int UNINITIALIZED_INT = -1;
44    // No legitimate long is negative
45    private static final long UNINITIALIZED_LONG = -1L;
46    // No legitimate float is negative
47    private static final float UNINITIALIZED_FLOAT = -1.0f;
48
49    public ReplayData readMotionEventData(final File file) {
50        final ReplayData replayData = new ReplayData();
51        try {
52            // Read file
53            final JsonReader jsonReader = new JsonReader(new BufferedReader(new InputStreamReader(
54                    new FileInputStream(file))));
55            jsonReader.beginArray();
56            while (jsonReader.hasNext()) {
57                readLogStatement(jsonReader, replayData);
58            }
59            jsonReader.endArray();
60        } catch (FileNotFoundException e) {
61            e.printStackTrace();
62        } catch (IOException e) {
63            e.printStackTrace();
64        }
65        return replayData;
66    }
67
68    @UsedForTesting
69    static class ReplayData {
70        final ArrayList<Integer> mActions = new ArrayList<Integer>();
71        final ArrayList<PointerProperties[]> mPointerPropertiesArrays
72                = new ArrayList<PointerProperties[]>();
73        final ArrayList<PointerCoords[]> mPointerCoordsArrays = new ArrayList<PointerCoords[]>();
74        final ArrayList<Long> mTimes = new ArrayList<Long>();
75    }
76
77    /**
78     * Read motion data from a logStatement and store it in {@code replayData}.
79     *
80     * Two kinds of logStatements can be read.  In the first variant, the MotionEvent data is
81     * represented as attributes at the top level like so:
82     *
83     * <pre>
84     * {
85     *   "_ct": 1359590400000,
86     *   "_ut": 4381933,
87     *   "_ty": "MotionEvent",
88     *   "action": "UP",
89     *   "isLoggingRelated": false,
90     *   "x": 100,
91     *   "y": 200
92     * }
93     * </pre>
94     *
95     * In the second variant, there is a separate attribute for the MotionEvent that includes
96     * historical data if present:
97     *
98     * <pre>
99     * {
100     *   "_ct": 135959040000,
101     *   "_ut": 4382702,
102     *   "_ty": "MotionEvent",
103     *   "action": "MOVE",
104     *   "isLoggingRelated": false,
105     *   "motionEvent": {
106     *     "pointerIds": [
107     *       0
108     *     ],
109     *     "xyt": [
110     *       {
111     *         "t": 4382551,
112     *         "d": [
113     *           {
114     *             "x": 141.25,
115     *             "y": 151.8485107421875,
116     *             "toma": 101.82337188720703,
117     *             "tomi": 101.82337188720703,
118     *             "o": 0.0
119     *           }
120     *         ]
121     *       },
122     *       {
123     *         "t": 4382559,
124     *         "d": [
125     *           {
126     *             "x": 140.7266082763672,
127     *             "y": 151.8485107421875,
128     *             "toma": 101.82337188720703,
129     *             "tomi": 101.82337188720703,
130     *             "o": 0.0
131     *           }
132     *         ]
133     *       }
134     *     ]
135     *   }
136     * },
137     * </pre>
138     */
139    @UsedForTesting
140    /* package for test */ void readLogStatement(final JsonReader jsonReader,
141            final ReplayData replayData) throws IOException {
142        String logStatementType = null;
143        int actionType = UNINITIALIZED_ACTION;
144        int x = UNINITIALIZED_INT;
145        int y = UNINITIALIZED_INT;
146        long time = UNINITIALIZED_LONG;
147        boolean isLoggingRelated = false;
148
149        jsonReader.beginObject();
150        while (jsonReader.hasNext()) {
151            final String key = jsonReader.nextName();
152            if (key.equals("_ty")) {
153                logStatementType = jsonReader.nextString();
154            } else if (key.equals("_ut")) {
155                time = jsonReader.nextLong();
156            } else if (key.equals("x")) {
157                x = jsonReader.nextInt();
158            } else if (key.equals("y")) {
159                y = jsonReader.nextInt();
160            } else if (key.equals("action")) {
161                final String s = jsonReader.nextString();
162                if (s.equals("UP")) {
163                    actionType = MotionEvent.ACTION_UP;
164                } else if (s.equals("DOWN")) {
165                    actionType = MotionEvent.ACTION_DOWN;
166                } else if (s.equals("MOVE")) {
167                    actionType = MotionEvent.ACTION_MOVE;
168                }
169            } else if (key.equals("loggingRelated")) {
170                isLoggingRelated = jsonReader.nextBoolean();
171            } else if (logStatementType != null && logStatementType.equals("MotionEvent")
172                    && key.equals("motionEvent")) {
173                if (actionType == UNINITIALIZED_ACTION) {
174                    Log.e(TAG, "no actionType assigned in MotionEvent json");
175                }
176                // Second variant of LogStatement.
177                if (isLoggingRelated) {
178                    jsonReader.skipValue();
179                } else {
180                    readEmbeddedMotionEvent(jsonReader, replayData, actionType);
181                }
182            } else {
183                if (DEBUG) {
184                    Log.w(TAG, "Unknown JSON key in LogStatement: " + key);
185                }
186                jsonReader.skipValue();
187            }
188        }
189        jsonReader.endObject();
190
191        if (logStatementType != null && time != UNINITIALIZED_LONG && x != UNINITIALIZED_INT
192                && y != UNINITIALIZED_INT && actionType != UNINITIALIZED_ACTION
193                && logStatementType.equals("MotionEvent") && !isLoggingRelated) {
194            // First variant of LogStatement.
195            final PointerProperties pointerProperties = new PointerProperties();
196            pointerProperties.id = 0;
197            pointerProperties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
198            final PointerProperties[] pointerPropertiesArray = {
199                pointerProperties
200            };
201            final PointerCoords pointerCoords = new PointerCoords();
202            pointerCoords.x = x;
203            pointerCoords.y = y;
204            pointerCoords.pressure = 1.0f;
205            pointerCoords.size = 1.0f;
206            final PointerCoords[] pointerCoordsArray = {
207                pointerCoords
208            };
209            addMotionEventData(replayData, actionType, time, pointerPropertiesArray,
210                    pointerCoordsArray);
211        }
212    }
213
214    private void readEmbeddedMotionEvent(final JsonReader jsonReader, final ReplayData replayData,
215            final int actionType) throws IOException {
216        jsonReader.beginObject();
217        PointerProperties[] pointerPropertiesArray = null;
218        while (jsonReader.hasNext()) {  // pointerIds/xyt
219            final String name = jsonReader.nextName();
220            if (name.equals("pointerIds")) {
221                pointerPropertiesArray = readPointerProperties(jsonReader);
222            } else if (name.equals("xyt")) {
223                readPointerData(jsonReader, replayData, actionType, pointerPropertiesArray);
224            }
225        }
226        jsonReader.endObject();
227    }
228
229    private PointerProperties[] readPointerProperties(final JsonReader jsonReader)
230            throws IOException {
231        final ArrayList<PointerProperties> pointerPropertiesArrayList =
232                new ArrayList<PointerProperties>();
233        jsonReader.beginArray();
234        while (jsonReader.hasNext()) {
235            final PointerProperties pointerProperties = new PointerProperties();
236            pointerProperties.id = jsonReader.nextInt();
237            pointerProperties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
238            pointerPropertiesArrayList.add(pointerProperties);
239        }
240        jsonReader.endArray();
241        return pointerPropertiesArrayList.toArray(
242                new PointerProperties[pointerPropertiesArrayList.size()]);
243    }
244
245    private void readPointerData(final JsonReader jsonReader, final ReplayData replayData,
246            final int actionType, final PointerProperties[] pointerPropertiesArray)
247            throws IOException {
248        if (pointerPropertiesArray == null) {
249            Log.e(TAG, "PointerIDs must be given before xyt data in json for MotionEvent");
250            jsonReader.skipValue();
251            return;
252        }
253        long time = UNINITIALIZED_LONG;
254        jsonReader.beginArray();
255        while (jsonReader.hasNext()) {  // Array of historical data
256            jsonReader.beginObject();
257            final ArrayList<PointerCoords> pointerCoordsArrayList = new ArrayList<PointerCoords>();
258            while (jsonReader.hasNext()) {  // Time/data object
259                final String name = jsonReader.nextName();
260                if (name.equals("t")) {
261                    time = jsonReader.nextLong();
262                } else if (name.equals("d")) {
263                    jsonReader.beginArray();
264                    while (jsonReader.hasNext()) {  // array of data per pointer
265                        final PointerCoords pointerCoords = readPointerCoords(jsonReader);
266                        if (pointerCoords != null) {
267                            pointerCoordsArrayList.add(pointerCoords);
268                        }
269                    }
270                    jsonReader.endArray();
271                } else {
272                    jsonReader.skipValue();
273                }
274            }
275            jsonReader.endObject();
276            // Data was recorded as historical events, but must be split apart into
277            // separate MotionEvents for replaying
278            if (time != UNINITIALIZED_LONG) {
279                addMotionEventData(replayData, actionType, time, pointerPropertiesArray,
280                        pointerCoordsArrayList.toArray(
281                                new PointerCoords[pointerCoordsArrayList.size()]));
282            } else {
283                Log.e(TAG, "Time not assigned in json for MotionEvent");
284            }
285        }
286        jsonReader.endArray();
287    }
288
289    private PointerCoords readPointerCoords(final JsonReader jsonReader) throws IOException {
290        jsonReader.beginObject();
291        float x = UNINITIALIZED_FLOAT;
292        float y = UNINITIALIZED_FLOAT;
293        while (jsonReader.hasNext()) {  // x,y
294            final String name = jsonReader.nextName();
295            if (name.equals("x")) {
296                x = (float) jsonReader.nextDouble();
297            } else if (name.equals("y")) {
298                y = (float) jsonReader.nextDouble();
299            } else {
300                jsonReader.skipValue();
301            }
302        }
303        jsonReader.endObject();
304
305        if (Float.compare(x, UNINITIALIZED_FLOAT) == 0
306                || Float.compare(y, UNINITIALIZED_FLOAT) == 0) {
307            Log.w(TAG, "missing x or y value in MotionEvent json");
308            return null;
309        }
310        final PointerCoords pointerCoords = new PointerCoords();
311        pointerCoords.x = x;
312        pointerCoords.y = y;
313        pointerCoords.pressure = 1.0f;
314        pointerCoords.size = 1.0f;
315        return pointerCoords;
316    }
317
318    /**
319     * Tests that {@code x} is uninitialized.
320     *
321     * Assumes that {@code x} will never be given a valid value less than 0, and that
322     * UNINITIALIZED_FLOAT is less than 0.0f.
323     */
324    private boolean isUninitializedFloat(final float x) {
325        return x < 0.0f;
326    }
327
328    private void addMotionEventData(final ReplayData replayData, final int actionType,
329            final long time, final PointerProperties[] pointerProperties,
330            final PointerCoords[] pointerCoords) {
331        replayData.mActions.add(actionType);
332        replayData.mTimes.add(time);
333        replayData.mPointerPropertiesArrays.add(pointerProperties);
334        replayData.mPointerCoordsArrays.add(pointerCoords);
335    }
336}
337