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