• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.inputmethod.research;
18 
19 import android.util.JsonReader;
20 import android.util.Log;
21 import android.view.MotionEvent;
22 import android.view.MotionEvent.PointerCoords;
23 import android.view.MotionEvent.PointerProperties;
24 
25 import com.android.inputmethod.annotations.UsedForTesting;
26 import com.android.inputmethod.latin.define.ProductionFlag;
27 
28 import java.io.BufferedReader;
29 import java.io.File;
30 import java.io.FileInputStream;
31 import java.io.FileNotFoundException;
32 import java.io.IOException;
33 import java.io.InputStreamReader;
34 import java.util.ArrayList;
35 
36 public 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 
readMotionEventData(final File file)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
readLogStatement(final JsonReader jsonReader, final ReplayData replayData)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 
readEmbeddedMotionEvent(final JsonReader jsonReader, final ReplayData replayData, final int actionType)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 
readPointerProperties(final JsonReader jsonReader)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 
readPointerData(final JsonReader jsonReader, final ReplayData replayData, final int actionType, final PointerProperties[] pointerPropertiesArray)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 
readPointerCoords(final JsonReader jsonReader)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      */
isUninitializedFloat(final float x)324     private boolean isUninitializedFloat(final float x) {
325         return x < 0.0f;
326     }
327 
addMotionEventData(final ReplayData replayData, final int actionType, final long time, final PointerProperties[] pointerProperties, final PointerCoords[] pointerCoords)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