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 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 private void addMotionEventData(final ReplayData replayData, final int actionType, 319 final long time, final PointerProperties[] pointerProperties, 320 final PointerCoords[] pointerCoords) { 321 replayData.mActions.add(actionType); 322 replayData.mTimes.add(time); 323 replayData.mPointerPropertiesArrays.add(pointerProperties); 324 replayData.mPointerCoordsArrays.add(pointerCoords); 325 } 326 } 327