/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.android.inputmethod.research; import android.util.JsonReader; import android.util.Log; import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; import android.view.MotionEvent.PointerProperties; import com.android.inputmethod.latin.define.ProductionFlag; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; public class MotionEventReader { private static final String TAG = MotionEventReader.class.getSimpleName(); private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG; // Assumes that MotionEvent.ACTION_MASK does not have all bits set.` private static final int UNINITIALIZED_ACTION = ~MotionEvent.ACTION_MASK; // No legitimate int is negative private static final int UNINITIALIZED_INT = -1; // No legitimate long is negative private static final long UNINITIALIZED_LONG = -1L; // No legitimate float is negative private static final float UNINITIALIZED_FLOAT = -1.0f; public ReplayData readMotionEventData(final File file) { final ReplayData replayData = new ReplayData(); try { // Read file final JsonReader jsonReader = new JsonReader(new BufferedReader(new InputStreamReader( new FileInputStream(file)))); jsonReader.beginArray(); while (jsonReader.hasNext()) { readLogStatement(jsonReader, replayData); } jsonReader.endArray(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return replayData; } static class ReplayData { final ArrayList mActions = new ArrayList(); final ArrayList mPointerPropertiesArrays = new ArrayList(); final ArrayList mPointerCoordsArrays = new ArrayList(); final ArrayList mTimes = new ArrayList(); } /** * Read motion data from a logStatement and store it in {@code replayData}. * * Two kinds of logStatements can be read. In the first variant, the MotionEvent data is * represented as attributes at the top level like so: * *
     * {
     *   "_ct": 1359590400000,
     *   "_ut": 4381933,
     *   "_ty": "MotionEvent",
     *   "action": "UP",
     *   "isLoggingRelated": false,
     *   "x": 100,
     *   "y": 200
     * }
     * 
* * In the second variant, there is a separate attribute for the MotionEvent that includes * historical data if present: * *
     * {
     *   "_ct": 135959040000,
     *   "_ut": 4382702,
     *   "_ty": "MotionEvent",
     *   "action": "MOVE",
     *   "isLoggingRelated": false,
     *   "motionEvent": {
     *     "pointerIds": [
     *       0
     *     ],
     *     "xyt": [
     *       {
     *         "t": 4382551,
     *         "d": [
     *           {
     *             "x": 141.25,
     *             "y": 151.8485107421875,
     *             "toma": 101.82337188720703,
     *             "tomi": 101.82337188720703,
     *             "o": 0.0
     *           }
     *         ]
     *       },
     *       {
     *         "t": 4382559,
     *         "d": [
     *           {
     *             "x": 140.7266082763672,
     *             "y": 151.8485107421875,
     *             "toma": 101.82337188720703,
     *             "tomi": 101.82337188720703,
     *             "o": 0.0
     *           }
     *         ]
     *       }
     *     ]
     *   }
     * },
     * 
*/ /* package for test */ void readLogStatement(final JsonReader jsonReader, final ReplayData replayData) throws IOException { String logStatementType = null; int actionType = UNINITIALIZED_ACTION; int x = UNINITIALIZED_INT; int y = UNINITIALIZED_INT; long time = UNINITIALIZED_LONG; boolean isLoggingRelated = false; jsonReader.beginObject(); while (jsonReader.hasNext()) { final String key = jsonReader.nextName(); if (key.equals("_ty")) { logStatementType = jsonReader.nextString(); } else if (key.equals("_ut")) { time = jsonReader.nextLong(); } else if (key.equals("x")) { x = jsonReader.nextInt(); } else if (key.equals("y")) { y = jsonReader.nextInt(); } else if (key.equals("action")) { final String s = jsonReader.nextString(); if (s.equals("UP")) { actionType = MotionEvent.ACTION_UP; } else if (s.equals("DOWN")) { actionType = MotionEvent.ACTION_DOWN; } else if (s.equals("MOVE")) { actionType = MotionEvent.ACTION_MOVE; } } else if (key.equals("loggingRelated")) { isLoggingRelated = jsonReader.nextBoolean(); } else if (logStatementType != null && logStatementType.equals("MotionEvent") && key.equals("motionEvent")) { if (actionType == UNINITIALIZED_ACTION) { Log.e(TAG, "no actionType assigned in MotionEvent json"); } // Second variant of LogStatement. if (isLoggingRelated) { jsonReader.skipValue(); } else { readEmbeddedMotionEvent(jsonReader, replayData, actionType); } } else { if (DEBUG) { Log.w(TAG, "Unknown JSON key in LogStatement: " + key); } jsonReader.skipValue(); } } jsonReader.endObject(); if (logStatementType != null && time != UNINITIALIZED_LONG && x != UNINITIALIZED_INT && y != UNINITIALIZED_INT && actionType != UNINITIALIZED_ACTION && logStatementType.equals("MotionEvent") && !isLoggingRelated) { // First variant of LogStatement. final PointerProperties pointerProperties = new PointerProperties(); pointerProperties.id = 0; pointerProperties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN; final PointerProperties[] pointerPropertiesArray = { pointerProperties }; final PointerCoords pointerCoords = new PointerCoords(); pointerCoords.x = x; pointerCoords.y = y; pointerCoords.pressure = 1.0f; pointerCoords.size = 1.0f; final PointerCoords[] pointerCoordsArray = { pointerCoords }; addMotionEventData(replayData, actionType, time, pointerPropertiesArray, pointerCoordsArray); } } private void readEmbeddedMotionEvent(final JsonReader jsonReader, final ReplayData replayData, final int actionType) throws IOException { jsonReader.beginObject(); PointerProperties[] pointerPropertiesArray = null; while (jsonReader.hasNext()) { // pointerIds/xyt final String name = jsonReader.nextName(); if (name.equals("pointerIds")) { pointerPropertiesArray = readPointerProperties(jsonReader); } else if (name.equals("xyt")) { readPointerData(jsonReader, replayData, actionType, pointerPropertiesArray); } } jsonReader.endObject(); } private PointerProperties[] readPointerProperties(final JsonReader jsonReader) throws IOException { final ArrayList pointerPropertiesArrayList = new ArrayList(); jsonReader.beginArray(); while (jsonReader.hasNext()) { final PointerProperties pointerProperties = new PointerProperties(); pointerProperties.id = jsonReader.nextInt(); pointerProperties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN; pointerPropertiesArrayList.add(pointerProperties); } jsonReader.endArray(); return pointerPropertiesArrayList.toArray( new PointerProperties[pointerPropertiesArrayList.size()]); } private void readPointerData(final JsonReader jsonReader, final ReplayData replayData, final int actionType, final PointerProperties[] pointerPropertiesArray) throws IOException { if (pointerPropertiesArray == null) { Log.e(TAG, "PointerIDs must be given before xyt data in json for MotionEvent"); jsonReader.skipValue(); return; } long time = UNINITIALIZED_LONG; jsonReader.beginArray(); while (jsonReader.hasNext()) { // Array of historical data jsonReader.beginObject(); final ArrayList pointerCoordsArrayList = new ArrayList(); while (jsonReader.hasNext()) { // Time/data object final String name = jsonReader.nextName(); if (name.equals("t")) { time = jsonReader.nextLong(); } else if (name.equals("d")) { jsonReader.beginArray(); while (jsonReader.hasNext()) { // array of data per pointer final PointerCoords pointerCoords = readPointerCoords(jsonReader); if (pointerCoords != null) { pointerCoordsArrayList.add(pointerCoords); } } jsonReader.endArray(); } else { jsonReader.skipValue(); } } jsonReader.endObject(); // Data was recorded as historical events, but must be split apart into // separate MotionEvents for replaying if (time != UNINITIALIZED_LONG) { addMotionEventData(replayData, actionType, time, pointerPropertiesArray, pointerCoordsArrayList.toArray( new PointerCoords[pointerCoordsArrayList.size()])); } else { Log.e(TAG, "Time not assigned in json for MotionEvent"); } } jsonReader.endArray(); } private PointerCoords readPointerCoords(final JsonReader jsonReader) throws IOException { jsonReader.beginObject(); float x = UNINITIALIZED_FLOAT; float y = UNINITIALIZED_FLOAT; while (jsonReader.hasNext()) { // x,y final String name = jsonReader.nextName(); if (name.equals("x")) { x = (float) jsonReader.nextDouble(); } else if (name.equals("y")) { y = (float) jsonReader.nextDouble(); } else { jsonReader.skipValue(); } } jsonReader.endObject(); if (Float.compare(x, UNINITIALIZED_FLOAT) == 0 || Float.compare(y, UNINITIALIZED_FLOAT) == 0) { Log.w(TAG, "missing x or y value in MotionEvent json"); return null; } final PointerCoords pointerCoords = new PointerCoords(); pointerCoords.x = x; pointerCoords.y = y; pointerCoords.pressure = 1.0f; pointerCoords.size = 1.0f; return pointerCoords; } /** * Tests that {@code x} is uninitialized. * * Assumes that {@code x} will never be given a valid value less than 0, and that * UNINITIALIZED_FLOAT is less than 0.0f. */ private boolean isUninitializedFloat(final float x) { return x < 0.0f; } private void addMotionEventData(final ReplayData replayData, final int actionType, final long time, final PointerProperties[] pointerProperties, final PointerCoords[] pointerCoords) { replayData.mActions.add(actionType); replayData.mTimes.add(time); replayData.mPointerPropertiesArrays.add(pointerProperties); replayData.mPointerCoordsArrays.add(pointerCoords); } }