Home | History | Annotate | Download | only in research
      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