1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of 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,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.cts.input;
18 
19 import static org.junit.Assert.assertEquals;
20 
21 import android.content.Context;
22 import android.hardware.lights.Light;
23 import android.util.ArrayMap;
24 import android.util.SparseArray;
25 import android.view.InputDevice;
26 import android.view.InputEvent;
27 import android.view.KeyEvent;
28 import android.view.MotionEvent;
29 
30 import org.json.JSONArray;
31 import org.json.JSONException;
32 import org.json.JSONObject;
33 
34 import java.io.ByteArrayOutputStream;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.io.OutputStream;
38 import java.util.ArrayList;
39 import java.util.Iterator;
40 import java.util.List;
41 
42 
43 /**
44  * Parse json resource file that contains the test commands for HidDevice
45  *
46  * For files containing reports and input events, each entry should be in the following format:
47  * <code>
48  * {"name": "test case name",
49  *  "reports": reports,
50  *  "events": input_events
51  * }
52  * </code>
53  *
54  * {@code reports} - an array of strings that contain hex arrays.
55  * {@code input_events} - an array of dicts in the following format:
56  * <code>
57  * {"action": "down|move|up", "axes": {"axis_x": x, "axis_y": y}, "keycode": "button_a"}
58  * </code>
59  * {@code "axes"} should only be defined for motion events, and {@code "keycode"} for key events.
60  * Timestamps will not be checked.
61 
62  * Example:
63  * <code>
64  * [{ "name": "press button A",
65  *    "reports": ["report1",
66  *                "report2",
67  *                "report3"
68  *               ],
69  *    "events": [{"action": "down", "axes": {"axis_y": 0.5, "axis_x": 0.1}},
70  *               {"action": "move", "axes": {"axis_y": 0.0, "axis_x": 0.0}}
71  *              ]
72  *  },
73  *  ... more tests like that
74  * ]
75  * </code>
76  */
77 public class InputJsonParser {
78     private static final String TAG = "InputJsonParser";
79 
80     private Context mContext;
81 
InputJsonParser(Context context)82     public InputJsonParser(Context context) {
83         mContext = context;
84     }
85 
86     /**
87      * Convenience function to create JSONArray from resource.
88      * The resource specified should contain JSON array as the top-level structure.
89      *
90      * @param resourceId The resourceId that contains the json data (typically inside R.raw)
91      */
getJsonArrayFromResource(int resourceId)92     private JSONArray getJsonArrayFromResource(int resourceId) {
93         String data = readRawResource(resourceId);
94         try {
95             return new JSONArray(data);
96         } catch (JSONException e) {
97             throw new RuntimeException(
98                     "Could not parse resource " + resourceId + ", received: " + data,
99                     e);
100         }
101     }
102 
103     /**
104      * Convenience function to read in an entire file as a String.
105      *
106      * @param id resourceId of the file
107      * @return contents of the raw resource file as a String
108      */
readRawResource(int id)109     private String readRawResource(int id) {
110         InputStream inputStream = mContext.getResources().openRawResource(id);
111         try {
112             return readFully(inputStream);
113         } catch (IOException e) {
114             throw new RuntimeException("Could not read resource id " + id);
115         }
116     }
117 
118     /**
119      * Read register command from raw resource.
120      *
121      * @param resourceId the raw resource id that contains the command
122      * @return the command to register device that can be passed to HidDevice constructor
123      */
readRegisterCommand(int resourceId)124     public String readRegisterCommand(int resourceId) {
125         return readRawResource(resourceId);
126     }
127 
128     /**
129      * Read entire input stream until no data remains.
130      *
131      * @param inputStream
132      * @return content of the input stream
133      * @throws IOException
134      */
readFully(InputStream inputStream)135     private String readFully(InputStream inputStream) throws IOException {
136         OutputStream baos = new ByteArrayOutputStream();
137         byte[] buffer = new byte[1024];
138         int read = inputStream.read(buffer);
139         while (read >= 0) {
140             baos.write(buffer, 0, read);
141             read = inputStream.read(buffer);
142         }
143         return baos.toString();
144     }
145 
146     /**
147      * Extract the device id from the raw resource file. This is needed in order to register
148      * a HidDevice.
149      *
150      * @param resourceId resource file that contains the register command.
151      * @return hid device id
152      */
readDeviceId(int resourceId)153     public int readDeviceId(int resourceId) {
154         try {
155             JSONObject json = new JSONObject(readRawResource(resourceId));
156             return json.getInt("id");
157         } catch (JSONException e) {
158             throw new RuntimeException("Could not read device id from resource " + resourceId);
159         }
160     }
161 
162     /**
163      * Extract the Vendor id from the raw resource file.
164      *
165      * @param resourceId resource file that contains the register command.
166      * @return device vendor id
167      */
readVendorId(int resourceId)168     public int readVendorId(int resourceId) {
169         try {
170             JSONObject json = new JSONObject(readRawResource(resourceId));
171             return json.getInt("vid");
172         } catch (JSONException e) {
173             throw new RuntimeException("Could not read vendor id from resource " + resourceId);
174         }
175     }
176 
177     /**
178      * Extract the input sources from the raw resource file.
179      *
180      * @param resourceId resource file that contains the register command.
181      * @return device sources
182      */
readSources(int resourceId)183     public int readSources(int resourceId) {
184         try {
185             JSONObject json = new JSONObject(readRawResource(resourceId));
186             return sourceFromString(json.optString("source"));
187         } catch (JSONException e) {
188             throw new RuntimeException("Could not read resource from resource " + resourceId);
189         }
190     }
191 
192     /**
193      * Extract the Product id from the raw resource file.
194      *
195      * @param resourceId resource file that contains the register command.
196      * @return device product id
197      */
readProductId(int resourceId)198     public int readProductId(int resourceId) {
199         try {
200             JSONObject json = new JSONObject(readRawResource(resourceId));
201             return json.getInt("pid");
202         } catch (JSONException e) {
203             throw new RuntimeException("Could not read prduct id from resource " + resourceId);
204         }
205     }
206 
getLongList(JSONArray array)207     private List<Long> getLongList(JSONArray array) {
208         List<Long> data = new ArrayList<Long>();
209         for (int i = 0; i < array.length(); i++) {
210             try {
211                 data.add(array.getLong(i));
212             } catch (JSONException e) {
213                 throw new RuntimeException("Could not read array index " + i);
214             }
215         }
216         return data;
217     }
218 
getIntList(JSONArray array)219     private List<Integer> getIntList(JSONArray array) {
220         List<Integer> data = new ArrayList<Integer>();
221         for (int i = 0; i < array.length(); i++) {
222             try {
223                 data.add(array.getInt(i));
224             } catch (JSONException e) {
225                 throw new RuntimeException("Could not read array index " + i);
226             }
227         }
228         return data;
229     }
230 
parseInputEvent(int testCaseNumber, int source, JSONObject entry)231     private InputEvent parseInputEvent(int testCaseNumber, int source, JSONObject entry) {
232         try {
233             InputEvent event;
234             if (entry.has("keycode")) {
235                 event = parseKeyEvent(source, entry);
236             } else if (entry.has("axes")) {
237                 event = parseMotionEvent(source, entry);
238             } else {
239                 throw new RuntimeException(
240                         "Input event is not specified correctly. Received: " + entry);
241             }
242             return event;
243         } catch (JSONException e) {
244             throw new RuntimeException("Could not process entry " + testCaseNumber + " : " + entry);
245         }
246     }
247 
248     /**
249      * Read json resource, and return a {@code List} of HidTestData, which contains
250      * the name of each test, along with the HID reports and the expected input events.
251      */
getHidTestData(int resourceId)252     public List<HidTestData> getHidTestData(int resourceId) {
253         JSONArray json = getJsonArrayFromResource(resourceId);
254         List<HidTestData> tests = new ArrayList<HidTestData>();
255         for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
256             HidTestData testData = new HidTestData();
257 
258             try {
259                 JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
260                 testData.name = testcaseEntry.getString("name");
261                 JSONArray reports = testcaseEntry.getJSONArray("reports");
262 
263                 for (int i = 0; i < reports.length(); i++) {
264                     String report = reports.getString(i);
265                     testData.reports.add(report);
266                 }
267 
268                 final int source = sourceFromString(testcaseEntry.optString("source"));
269                 JSONArray events = testcaseEntry.getJSONArray("events");
270                 for (int i = 0; i < events.length(); i++) {
271                     testData.events.add(parseInputEvent(i, source, events.getJSONObject(i)));
272                 }
273                 tests.add(testData);
274             } catch (JSONException e) {
275                 throw new RuntimeException("Could not process entry " + testCaseNumber);
276             }
277         }
278         return tests;
279     }
280 
281     /**
282      * Read json resource, and return a {@code List} of HidVibratorTestData, which contains
283      * the vibrator FF effect strength data index, and the hid output verification data.
284      */
getHidVibratorTestData(int resourceId)285     public List<HidVibratorTestData> getHidVibratorTestData(int resourceId) {
286         JSONArray json = getJsonArrayFromResource(resourceId);
287         List<HidVibratorTestData> tests = new ArrayList<HidVibratorTestData>();
288         for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
289             HidVibratorTestData testData = new HidVibratorTestData();
290             try {
291                 JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
292                 testData.leftFfIndex = testcaseEntry.getInt("leftFfIndex");
293                 testData.rightFfIndex = testcaseEntry.getInt("rightFfIndex");
294 
295                 JSONArray durationsArray = testcaseEntry.getJSONArray("durations");
296                 JSONArray amplitudesArray = testcaseEntry.getJSONArray("amplitudes");
297                 assertEquals(durationsArray.length(), amplitudesArray.length());
298                 testData.durations = new ArrayList<Long>();
299                 testData.amplitudes = new ArrayList<Integer>();
300                 for (int i = 0; i < durationsArray.length(); i++) {
301                     testData.durations.add(durationsArray.getLong(i));
302                     testData.amplitudes.add(amplitudesArray.getInt(i));
303                 }
304 
305                 JSONArray outputArray = testcaseEntry.getJSONArray("output");
306                 testData.verifyMap = new ArrayMap<Integer, Integer>();
307                 for (int i = 0; i < outputArray.length(); i++) {
308                     JSONObject item = outputArray.getJSONObject(i);
309                     int index = item.getInt("index");
310                     int data = item.getInt("data");
311                     testData.verifyMap.put(index, data);
312                 }
313                 tests.add(testData);
314             } catch (JSONException e) {
315                 throw new RuntimeException("Could not process entry " + testCaseNumber);
316             }
317         }
318         return tests;
319     }
320 
321     /**
322      * Read json resource, and return a {@code List} of HidBatteryTestData, which contains
323      * the name of each test, along with the HID reports and the expected batttery status.
324      */
getHidBatteryTestData(int resourceId)325     public List<HidBatteryTestData> getHidBatteryTestData(int resourceId) {
326         JSONArray json = getJsonArrayFromResource(resourceId);
327         List<HidBatteryTestData> tests = new ArrayList<HidBatteryTestData>();
328         for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
329             HidBatteryTestData testData = new HidBatteryTestData();
330             try {
331                 JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
332                 testData.name = testcaseEntry.getString("name");
333                 JSONArray reports = testcaseEntry.getJSONArray("reports");
334 
335                 for (int i = 0; i < reports.length(); i++) {
336                     String report = reports.getString(i);
337                     testData.reports.add(report);
338                 }
339 
340                 JSONArray capacitiesArray = testcaseEntry.getJSONArray("capacities");
341                 testData.capacities = new float[capacitiesArray.length()];
342                 for (int i = 0; i < capacitiesArray.length(); i++) {
343                     testData.capacities[i] = Float.valueOf(capacitiesArray.getString(i));
344                 }
345                 testData.status = testcaseEntry.getInt("status");
346                 tests.add(testData);
347             } catch (JSONException e) {
348                 throw new RuntimeException("Could not process entry " + testCaseNumber + " " + e);
349             }
350         }
351         return tests;
352     }
353 
354     /**
355      * Read json resource, and return a {@code List} of HidLightTestData, which contains
356      * the light type and light state request, and the hid output verification data.
357      */
getHidLightTestData(int resourceId)358     public List<HidLightTestData> getHidLightTestData(int resourceId) {
359         JSONArray json = getJsonArrayFromResource(resourceId);
360         List<HidLightTestData> tests = new ArrayList<HidLightTestData>();
361         for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
362             HidLightTestData testData = new HidLightTestData();
363             try {
364                 JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
365                 testData.lightType = lightTypeFromString(
366                     testcaseEntry.getString("type"));
367                 testData.lightCapabilities = lightCapabilityFromString(
368                         testcaseEntry.getString("capabilities"));
369                 testData.lightName = testcaseEntry.getString("name");
370                 testData.lightColor = testcaseEntry.getInt("color");
371                 testData.lightPlayerId = testcaseEntry.getInt("playerId");
372                 testData.hidEventType = uhidEventFromString(
373                         testcaseEntry.getString("hidEventType"));
374                 testData.report = testcaseEntry.getString("report");
375 
376                 JSONArray outputArray = testcaseEntry.getJSONArray("ledsHidOutput");
377                 testData.expectedHidData = new ArrayMap<Integer, Integer>();
378                 for (int i = 0; i < outputArray.length(); i++) {
379                     JSONObject item = outputArray.getJSONObject(i);
380                     int index = item.getInt("index");
381                     int data = item.getInt("data");
382                     testData.expectedHidData.put(index, data);
383                 }
384                 tests.add(testData);
385             } catch (JSONException e) {
386                 throw new RuntimeException("Could not process entry " + testCaseNumber + " : " + e);
387             }
388         }
389         return tests;
390     }
391 
392     /**
393      * Read json resource, and return a {@code List} of UinputVibratorTestData, which contains
394      * the vibrator FF effect of durations and amplitudes.
395      */
getUinputVibratorTestData(int resourceId)396     public List<UinputVibratorTestData> getUinputVibratorTestData(int resourceId) {
397         JSONArray json = getJsonArrayFromResource(resourceId);
398         List<UinputVibratorTestData> tests = new ArrayList<UinputVibratorTestData>();
399         for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
400             UinputVibratorTestData testData = new UinputVibratorTestData();
401             try {
402                 JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
403                 JSONArray durationsArray = testcaseEntry.getJSONArray("durations");
404                 JSONArray amplitudesArray = testcaseEntry.getJSONArray("amplitudes");
405                 assertEquals("Duration array length not equal to amplitude array length",
406                         durationsArray.length(), amplitudesArray.length());
407                 testData.durations = getLongList(durationsArray);
408                 testData.amplitudes = getIntList(amplitudesArray);
409                 tests.add(testData);
410             } catch (JSONException e) {
411                 throw new RuntimeException("Could not process entry " + testCaseNumber);
412             }
413         }
414         return tests;
415     }
416 
417     /**
418      * Read json resource, and return a {@code List} of UinputVibratorManagerTestData, which
419      * contains the vibrator Ids and FF effect of durations and amplitudes.
420      */
getUinputVibratorManagerTestData(int resourceId)421     public List<UinputVibratorManagerTestData> getUinputVibratorManagerTestData(int resourceId) {
422         JSONArray json = getJsonArrayFromResource(resourceId);
423         List<UinputVibratorManagerTestData> tests = new ArrayList<UinputVibratorManagerTestData>();
424         for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
425             UinputVibratorManagerTestData testData = new UinputVibratorManagerTestData();
426             try {
427                 JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
428                 JSONArray durationsArray = testcaseEntry.getJSONArray("durations");
429                 testData.durations = getLongList(durationsArray);
430                 testData.amplitudes = new SparseArray<>();
431                 JSONObject amplitudesObj = testcaseEntry.getJSONObject("amplitudes");
432                 for (int i = 0; i < amplitudesObj.names().length(); i++) {
433                     String vibratorId = amplitudesObj.names().getString(i);
434                     JSONArray amplitudesArray = amplitudesObj.getJSONArray(vibratorId);
435                     testData.amplitudes.append(Integer.valueOf(vibratorId),
436                             getIntList(amplitudesArray));
437                     assertEquals("Duration array length not equal to amplitude array length",
438                             durationsArray.length(), amplitudesArray.length());
439                 }
440                 tests.add(testData);
441             } catch (JSONException e) {
442                 throw new RuntimeException("Could not process entry " + testCaseNumber);
443             }
444         }
445         return tests;
446     }
447 
448     /**
449      * Read json resource, and return a {@code List} of UinputTestData, which contains
450      * the name of each test, along with the uinput injections and the expected input events.
451      */
getUinputTestData(int resourceId)452     public List<UinputTestData> getUinputTestData(int resourceId) {
453         JSONArray json = getJsonArrayFromResource(resourceId);
454         List<UinputTestData> tests = new ArrayList<UinputTestData>();
455         for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
456             UinputTestData testData = new UinputTestData();
457 
458             try {
459                 JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
460                 testData.name = testcaseEntry.getString("name");
461                 JSONArray reports = testcaseEntry.getJSONArray("injections");
462                 for (int i = 0; i < reports.length(); i++) {
463                     String injections = reports.getString(i);
464                     testData.evdevEvents.add(injections);
465                 }
466 
467                 final int source = sourceFromString(testcaseEntry.optString("source"));
468 
469                 JSONArray events = testcaseEntry.getJSONArray("events");
470                 for (int i = 0; i < events.length(); i++) {
471                     testData.events.add(parseInputEvent(i, source, events.getJSONObject(i)));
472                 }
473                 tests.add(testData);
474             } catch (JSONException e) {
475                 throw new RuntimeException("Could not process entry " + testCaseNumber);
476             }
477         }
478         return tests;
479     }
480 
parseKeyEvent(int source, JSONObject entry)481     private KeyEvent parseKeyEvent(int source, JSONObject entry) throws JSONException {
482         int action = keyActionFromString(entry.getString("action"));
483         int keyCode = KeyEvent.keyCodeFromString(entry.getString("keycode"));
484         int metaState = metaStateFromString(entry.optString("metaState"));
485         // We will only check select fields of the KeyEvent. Times are not checked.
486         return new KeyEvent(/* downTime */ 0, /* eventTime */ 0, action, keyCode,
487                 /* repeat */ 0, metaState, /* deviceId */ 0, /* scanCode */ 0,
488                 /* flags */ 0, source);
489     }
490 
parseMotionEvent(int source, JSONObject entry)491     private MotionEvent parseMotionEvent(int source, JSONObject entry) throws JSONException {
492         JSONArray pointers = entry.optJSONArray("axes");
493         int pointerCount = pointers == null ? 1 : pointers.length();
494 
495         MotionEvent.PointerProperties[] properties =
496                 new MotionEvent.PointerProperties[pointerCount];
497         for (int i = 0; i < pointerCount; i++) {
498             properties[i] = new MotionEvent.PointerProperties();
499             properties[i].id = i;
500             properties[i].toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
501         }
502 
503         MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[pointerCount];
504         for (int i = 0; i < pointerCount; i++) {
505             coords[i] = new MotionEvent.PointerCoords();
506         }
507 
508         int action = motionActionFromString(entry);
509 
510         int buttonState = 0;
511         JSONArray buttons = entry.optJSONArray("buttonState");
512         if (buttons != null) {
513             for (int i = 0; i < buttons.length(); i++) {
514                 String buttonStr = buttons.getString(i);
515                 buttonState |= motionButtonFromString(buttonStr);
516             }
517         }
518 
519         // "axes" field should be an array if there are multiple pointers
520         for (int i = 0; i < pointerCount; i++) {
521             JSONObject axes;
522             if (pointers == null) {
523                 axes = entry.getJSONObject("axes");
524             } else {
525                 axes = pointers.getJSONObject(i);
526             }
527             Iterator<String> keys = axes.keys();
528             while (keys.hasNext()) {
529                 String axis = keys.next();
530                 float value = (float) axes.getDouble(axis);
531                 coords[i].setAxisValue(MotionEvent.axisFromString(axis), value);
532             }
533         }
534 
535         // Times are not checked
536         return MotionEvent.obtain(/* downTime */ 0, /* eventTime */ 0, action,
537                 pointerCount, properties, coords, 0, buttonState, 0f, 0f,
538                 0, 0, source, 0);
539     }
540 
keyActionFromString(String action)541     private static int keyActionFromString(String action) {
542         switch (action.toUpperCase()) {
543             case "DOWN":
544                 return KeyEvent.ACTION_DOWN;
545             case "UP":
546                 return KeyEvent.ACTION_UP;
547         }
548         throw new RuntimeException("Unknown action specified: " + action);
549     }
550 
metaStateFromString(String metaStateString)551     private static int metaStateFromString(String metaStateString) {
552         int metaState = 0;
553         if (metaStateString.isEmpty()) {
554             return metaState;
555         }
556         final String[] metaKeys = metaStateString.split("\\|");
557         for (final String metaKeyString : metaKeys) {
558             final String trimmedKeyString = metaKeyString.trim();
559             switch (trimmedKeyString.toUpperCase()) {
560                 case "SHIFT_LEFT":
561                     metaState |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON;
562                     break;
563                 case "SHIFT_RIGHT":
564                     metaState |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_RIGHT_ON;
565                     break;
566                 case "CTRL_LEFT":
567                     metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
568                     break;
569                 case "CTRL_RIGHT":
570                     metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_RIGHT_ON;
571                     break;
572                 case "ALT_LEFT":
573                     metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
574                     break;
575                 case "ALT_RIGHT":
576                     metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON;
577                     break;
578                 case "META_LEFT":
579                     metaState |= KeyEvent.META_META_ON | KeyEvent.META_META_LEFT_ON;
580                     break;
581                 case "META_RIGHT":
582                     metaState |= KeyEvent.META_META_ON | KeyEvent.META_META_RIGHT_ON;
583                     break;
584                 case "CAPS_LOCK":
585                     metaState |= KeyEvent.META_CAPS_LOCK_ON;
586                     break;
587                 case "NUM_LOCK":
588                     metaState |= KeyEvent.META_NUM_LOCK_ON;
589                     break;
590                 case "SCROLL_LOCK":
591                     metaState |= KeyEvent.META_SCROLL_LOCK_ON;
592                     break;
593                 default:
594                     throw new RuntimeException("Unknown meta state chunk: " + trimmedKeyString
595                             + " in meta state string: " + metaStateString);
596             }
597         }
598         return metaState;
599     }
600 
motionActionFromString(JSONObject entry)601     private static int motionActionFromString(JSONObject entry) {
602         String action;
603         int motionAction = 0;
604 
605         try {
606             action = entry.getString("action").toUpperCase();
607         } catch (JSONException e) {
608             throw new RuntimeException("Action not specified. ");
609         }
610 
611         switch (action) {
612             case "DOWN":
613                 motionAction = MotionEvent.ACTION_DOWN;
614                 break;
615             case "MOVE":
616                 motionAction = MotionEvent.ACTION_MOVE;
617                 break;
618             case "UP":
619                 motionAction = MotionEvent.ACTION_UP;
620                 break;
621             case "BUTTON_PRESS":
622                 motionAction = MotionEvent.ACTION_BUTTON_PRESS;
623                 break;
624             case "BUTTON_RELEASE":
625                 motionAction = MotionEvent.ACTION_BUTTON_RELEASE;
626                 break;
627             case "HOVER_ENTER":
628                 motionAction = MotionEvent.ACTION_HOVER_ENTER;
629                 break;
630             case "HOVER_MOVE":
631                 motionAction = MotionEvent.ACTION_HOVER_MOVE;
632                 break;
633             case "HOVER_EXIT":
634                 motionAction = MotionEvent.ACTION_HOVER_EXIT;
635                 break;
636             case "POINTER_DOWN":
637                 motionAction = MotionEvent.ACTION_POINTER_DOWN;
638                 break;
639             case "POINTER_UP":
640                 motionAction = MotionEvent.ACTION_POINTER_UP;
641                 break;
642             case "CANCEL":
643                 motionAction = MotionEvent.ACTION_CANCEL;
644                 break;
645             default:
646                 throw new RuntimeException("Unknown action specified: " + action);
647         }
648         int pointerId;
649         try {
650             if (motionAction == MotionEvent.ACTION_POINTER_UP
651                     || motionAction == MotionEvent.ACTION_POINTER_DOWN) {
652                 pointerId = entry.getInt("pointerId");
653             } else {
654                 pointerId = entry.optInt("pointerId", 0);
655             }
656         } catch (JSONException e) {
657             throw new RuntimeException("PointerId not specified: " + action);
658         }
659         return motionAction | (pointerId << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
660     }
661 
sourceFromString(String sourceString)662     private static int sourceFromString(String sourceString) {
663         if (sourceString.isEmpty()) {
664             return InputDevice.SOURCE_UNKNOWN;
665         }
666         int source = 0;
667         final String[] sourceEntries = sourceString.split("\\|");
668         for (final String sourceEntry : sourceEntries) {
669             final String trimmedSourceEntry = sourceEntry.trim();
670             switch (trimmedSourceEntry.toUpperCase()) {
671                 case "MOUSE":
672                     source |= InputDevice.SOURCE_MOUSE;
673                     break;
674                 case "MOUSE_RELATIVE":
675                     source |= InputDevice.SOURCE_MOUSE_RELATIVE;
676                     break;
677                 case "JOYSTICK":
678                     source |= InputDevice.SOURCE_JOYSTICK;
679                     break;
680                 case "KEYBOARD":
681                     source |= InputDevice.SOURCE_KEYBOARD;
682                     break;
683                 case "GAMEPAD":
684                     source |= InputDevice.SOURCE_GAMEPAD;
685                     break;
686                 case "DPAD":
687                     source |= InputDevice.SOURCE_DPAD;
688                     break;
689                 case "TOUCHPAD":
690                     source |= InputDevice.SOURCE_TOUCHPAD;
691                     break;
692                 case "SENSOR":
693                     source |= InputDevice.SOURCE_SENSOR;
694                     break;
695                 default:
696                     throw new RuntimeException("Unknown source chunk: " + trimmedSourceEntry
697                             + " in source string: " + sourceString);
698             }
699         }
700         return source;
701     }
702 
motionButtonFromString(String button)703     private static int motionButtonFromString(String button) {
704         switch (button.toUpperCase()) {
705             case "BACK":
706                 return MotionEvent.BUTTON_BACK;
707             case "FORWARD":
708                 return MotionEvent.BUTTON_FORWARD;
709             case "PRIMARY":
710                 return MotionEvent.BUTTON_PRIMARY;
711             case "SECONDARY":
712                 return MotionEvent.BUTTON_SECONDARY;
713             case "STYLUS_PRIMARY":
714                 return MotionEvent.BUTTON_STYLUS_PRIMARY;
715             case "STYLUS_SECONDARY":
716                 return MotionEvent.BUTTON_STYLUS_SECONDARY;
717             case "TERTIARY":
718                 return MotionEvent.BUTTON_TERTIARY;
719         }
720         throw new RuntimeException("Unknown button specified: " + button);
721     }
722 
lightTypeFromString(String typeString)723     private static int lightTypeFromString(String typeString) {
724         switch (typeString.toUpperCase()) {
725             case "INPUT":
726                 return Light.LIGHT_TYPE_INPUT;
727             case "PLAYER_ID":
728                 return Light.LIGHT_TYPE_PLAYER_ID;
729             default:
730                 throw new RuntimeException("Unknown light type specified: " + typeString);
731         }
732     }
733 
lightCapabilityFromString(String capString)734     private static int lightCapabilityFromString(String capString) {
735         int capabilities = 0;
736         final String[] entries = capString.split("\\|");
737         for (final String entry : entries) {
738             final String trimmedEntry = entry.trim();
739             switch (trimmedEntry.toUpperCase()) {
740                 case "BRIGHTNESS":
741                     capabilities |= Light.LIGHT_CAPABILITY_BRIGHTNESS;
742                     break;
743                 case "RGB":
744                     capabilities |= Light.LIGHT_CAPABILITY_RGB;
745                     break;
746                 case "NONE":
747                     break;
748                 default:
749                     throw new RuntimeException("Unknown capability specified: " + capString);
750             }
751         }
752         return capabilities;
753     }
754 
755     // Return the enum uhid_event_type in kernel linux/uhid.h.
uhidEventFromString(String eventString)756     private static byte uhidEventFromString(String eventString) {
757         switch (eventString.toUpperCase()) {
758             case "UHID_OUTPUT":
759                 return 6;
760             case "UHID_GET_REPORT":
761                 return 9;
762             case "UHID_SET_REPORT":
763                 return 13;
764         }
765         throw new RuntimeException("Unknown uhid event type specified: " + eventString);
766     }
767 }
768