• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2023 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.commands.uinput;
18 
19 import android.annotation.Nullable;
20 import android.util.SparseArray;
21 
22 import java.io.IOException;
23 import java.io.LineNumberReader;
24 import java.io.Reader;
25 import java.util.ArrayDeque;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Queue;
32 
33 import src.com.android.commands.uinput.InputAbsInfo;
34 
35 /**
36  * Parser for the <a href="https://gitlab.freedesktop.org/libevdev/evemu">FreeDesktop evemu</a>
37  * event recording format.
38  */
39 public class EvemuParser implements EventParser {
40     private static final String TAG = "UinputEvemuParser";
41 
42     /**
43      * The device ID to use for all events. Since evemu files only support single-device
44      * recordings, this will always be the same.
45      */
46     private static final int DEVICE_ID = 1;
47     private static final int REGISTRATION_DELAY_NANOS = 500_000_000;
48 
49     private static class CommentAwareReader {
50         private final LineNumberReader mReader;
51         private String mPreviousLine;
52         private String mNextLine;
53 
CommentAwareReader(LineNumberReader in)54         CommentAwareReader(LineNumberReader in) throws IOException {
55             mReader = in;
56             mNextLine = findNextLine();
57         }
58 
findNextLine()59         private @Nullable String findNextLine() throws IOException {
60             String line = "";
61             while (line != null && line.length() == 0) {
62                 String unstrippedLine = mReader.readLine();
63                 if (unstrippedLine == null) {
64                     // End of file.
65                     return null;
66                 }
67                 line = stripComments(unstrippedLine);
68             }
69             return line;
70         }
71 
stripComments(String line)72         private static String stripComments(String line) {
73             int index = line.indexOf('#');
74             // 'N:' lines (which contain the name of the input device) do not support trailing
75             // comments, to support recording device names that contain #s.
76             if (index < 0 || line.startsWith("N: ")) {
77                 return line;
78             } else {
79                 return line.substring(0, index).strip();
80             }
81         }
82 
83         /**
84          * Returns the next line of the file that isn't blank when stripped of comments, or
85          * {@code null} if the end of the file is reached. However, it does not advance to the
86          * next line of the file.
87          */
peekLine()88         public @Nullable String peekLine() {
89             return mNextLine;
90         }
91 
92         /** Moves to the next line of the file. */
advance()93         public void advance() throws IOException {
94             mPreviousLine = mNextLine;
95             mNextLine = findNextLine();
96         }
97 
isAtEndOfFile()98         public boolean isAtEndOfFile() {
99             return mNextLine == null;
100         }
101 
102         /** Returns the previous line, for error messages. */
getPreviousLine()103         public String getPreviousLine() {
104             return mPreviousLine;
105         }
106 
107         /** Returns the number of the <b>previous</b> line. */
getPreviousLineNumber()108         public int getPreviousLineNumber() {
109             return mReader.getLineNumber() - 1;
110         }
111     }
112 
113     public static class ParsingException extends RuntimeException {
114         private final int mLineNumber;
115         private final String mLine;
116 
ParsingException(String message, CommentAwareReader reader)117         ParsingException(String message, CommentAwareReader reader) {
118             this(message, reader.getPreviousLine(), reader.getPreviousLineNumber());
119         }
120 
ParsingException(String message, String line, int lineNumber)121         ParsingException(String message, String line, int lineNumber) {
122             super(message);
123             mLineNumber = lineNumber;
124             mLine = line;
125         }
126 
127         /** Returns a nicely formatted error message, including the line number and line. */
makeErrorMessage()128         public String makeErrorMessage() {
129             return String.format("""
130                     Parsing error on line %d: %s
131                     --> %s
132                     """, mLineNumber, getMessage(), mLine);
133         }
134     }
135 
136     private final CommentAwareReader mReader;
137     /**
138      * The timestamp of the last event returned, of the head of {@link #mQueuedEvents} if there is
139      * one, or -1 if no events have been returned yet.
140      */
141     private long mLastEventTimeMicros = -1;
142     private final Queue<Event> mQueuedEvents = new ArrayDeque<>(2);
143 
EvemuParser(Reader in)144     public EvemuParser(Reader in) throws IOException {
145         mReader = new CommentAwareReader(new LineNumberReader(in));
146         mQueuedEvents.add(parseRegistrationEvent());
147 
148         // The kernel takes a little time to set up an evdev device after the initial
149         // registration. Any events that we try to inject during this period would be silently
150         // dropped, so we delay for a short period after registration and before injecting any
151         // events.
152         final Event.Builder delayEb = new Event.Builder();
153         delayEb.setId(DEVICE_ID);
154         delayEb.setCommand(Event.Command.DELAY);
155         delayEb.setDurationNanos(REGISTRATION_DELAY_NANOS);
156         mQueuedEvents.add(delayEb.build());
157     }
158 
159     /**
160      * Returns the next event in the evemu recording.
161      */
getNextEvent()162     public Event getNextEvent() throws IOException {
163         if (!mQueuedEvents.isEmpty()) {
164             return mQueuedEvents.remove();
165         }
166 
167         if (mReader.isAtEndOfFile()) {
168             return null;
169         }
170 
171         final String line = expectLine("E");
172         final String[] parts = expectParts(line, 4);
173         final String[] timeParts = parts[0].split("\\.");
174         if (timeParts.length != 2) {
175             throw new ParsingException(
176                     "Invalid timestamp '" + parts[0] + "' (should contain a single '.')", mReader);
177         }
178         final long timeMicros =
179                 parseLong(timeParts[0], 10) * 1_000_000 + parseInt(timeParts[1], 10);
180         final Event.Builder eb = new Event.Builder();
181         eb.setId(DEVICE_ID);
182         eb.setCommand(Event.Command.INJECT);
183         final int eventType = parseInt(parts[1], 16);
184         final int eventCode = parseInt(parts[2], 16);
185         final int value = parseInt(parts[3], 10);
186         eb.setInjections(new int[] {eventType, eventCode, value});
187 
188         if (mLastEventTimeMicros == -1) {
189             // This is the first event being injected, so send it straight away.
190             mLastEventTimeMicros = timeMicros;
191             return eb.build();
192         } else {
193             final long delayMicros = timeMicros - mLastEventTimeMicros;
194             eb.setTimestampOffsetMicros(delayMicros);
195             if (delayMicros == 0) {
196                 return eb.build();
197             }
198             // Send a delay now, and queue the actual event for the next call.
199             mQueuedEvents.add(eb.build());
200             mLastEventTimeMicros = timeMicros;
201             final Event.Builder delayEb = new Event.Builder();
202             delayEb.setId(DEVICE_ID);
203             delayEb.setCommand(Event.Command.DELAY);
204             delayEb.setDurationNanos(delayMicros * 1000);
205             return delayEb.build();
206         }
207     }
208 
parseRegistrationEvent()209     private Event parseRegistrationEvent() throws IOException {
210         // The registration details at the start of a recording are specified by a set of lines
211         // that have to be in this order: N, I, P, B, A, L, S. Recordings must have exactly one N
212         // (name) and I (IDs) line. The remaining lines are optional, and there may be multiple
213         // of those lines.
214 
215         final Event.Builder eb = new Event.Builder();
216         eb.setId(DEVICE_ID);
217         eb.setCommand(Event.Command.REGISTER);
218         eb.setName(expectLine("N"));
219 
220         final String idsLine = expectLine("I");
221         final String[] idStrings = expectParts(idsLine, 4);
222         eb.setBusId(parseInt(idStrings[0], 16));
223         eb.setVendorId(parseInt(idStrings[1], 16));
224         eb.setProductId(parseInt(idStrings[2], 16));
225         eb.setVersionId(parseInt(idStrings[3], 16));
226 
227         final SparseArray<int[]> config = new SparseArray<>();
228         config.append(Event.UinputControlCode.UI_SET_PROPBIT.getValue(), parseProperties());
229 
230         parseAxisBitmaps(config);
231 
232         eb.setConfiguration(config);
233         if (config.contains(Event.UinputControlCode.UI_SET_FFBIT.getValue())) {
234             // If the device specifies any force feedback effects, the kernel will require the
235             // ff_effects_max value to be set.
236             eb.setFfEffectsMax(config.get(Event.UinputControlCode.UI_SET_FFBIT.getValue()).length);
237         }
238 
239         eb.setAbsInfo(parseAbsInfos());
240 
241         // L: and S: lines allow the initial states of the device's LEDs and switches to be
242         // recorded. However, the FreeDesktop implementation doesn't support actually setting these
243         // states at the start of playback (apparently due to concerns over race conditions), and we
244         // have no need for this feature either, so for now just skip over them.
245         skipUnsupportedLines("L");
246         skipUnsupportedLines("S");
247 
248         return eb.build();
249     }
250 
parseProperties()251     private int[] parseProperties() throws IOException {
252         final ArrayList<Integer> propBitmapParts = new ArrayList<>();
253         String line = acceptLine("P");
254         while (line != null) {
255             String[] parts = line.strip().split(" ");
256             propBitmapParts.ensureCapacity(propBitmapParts.size() + parts.length);
257             for (String part : parts) {
258                 propBitmapParts.add(parseBitmapPart(part, line));
259             }
260             line = acceptLine("P");
261         }
262         return bitmapToEventCodes(propBitmapParts);
263     }
264 
parseAxisBitmaps(SparseArray<int[]> config)265     private void parseAxisBitmaps(SparseArray<int[]> config) throws IOException {
266         final Map<Integer, ArrayList<Integer>> axisBitmapParts = new HashMap<>();
267         String line = acceptLine("B");
268         while (line != null) {
269             final String[] parts = line.strip().split(" ");
270             if (parts.length < 2) {
271                 throw new ParsingException(
272                         "Expected event type and at least one bitmap byte on 'B:' line; only found "
273                                 + parts.length + " elements", mReader);
274             }
275             final int eventType = parseInt(parts[0], 16);
276             // EV_SYN cannot be configured through uinput, so skip it.
277             if (eventType != Event.EV_SYN) {
278                 if (!axisBitmapParts.containsKey(eventType)) {
279                     axisBitmapParts.put(eventType, new ArrayList<>());
280                 }
281                 ArrayList<Integer> bitmapParts = axisBitmapParts.get(eventType);
282                 bitmapParts.ensureCapacity(bitmapParts.size() + parts.length);
283                 for (int i = 1; i < parts.length; i++) {
284                     axisBitmapParts.get(eventType).add(parseBitmapPart(parts[i], line));
285                 }
286             }
287             line = acceptLine("B");
288         }
289         final List<Integer> eventTypesToSet = new ArrayList<>();
290         for (var entry : axisBitmapParts.entrySet()) {
291             if (entry.getValue().size() == 0) {
292                 continue;
293             }
294             final Event.UinputControlCode controlCode =
295                     Event.UinputControlCode.forEventType(entry.getKey());
296             final int[] eventCodes = bitmapToEventCodes(entry.getValue());
297             if (controlCode != null && eventCodes.length > 0) {
298                 config.append(controlCode.getValue(), eventCodes);
299                 eventTypesToSet.add(entry.getKey());
300             }
301         }
302         config.append(
303                 Event.UinputControlCode.UI_SET_EVBIT.getValue(), unboxIntList(eventTypesToSet));
304     }
305 
parseBitmapPart(String part, String line)306     private int parseBitmapPart(String part, String line) {
307         int b = parseInt(part, 16);
308         if (b < 0x0 || b > 0xff) {
309             throw new ParsingException("Bitmap part '" + part
310                     + "' invalid; parts must be hexadecimal values between 00 and ff.", mReader);
311         }
312         return b;
313     }
314 
parseAbsInfos()315     private SparseArray<InputAbsInfo> parseAbsInfos() throws IOException {
316         final SparseArray<InputAbsInfo> absInfos = new SparseArray<>();
317         String line = acceptLine("A");
318         while (line != null) {
319             final String[] parts = line.strip().split(" ");
320             if (parts.length < 5 || parts.length > 6) {
321                 throw new ParsingException(
322                         "AbsInfo lines should have the format 'A: <index (hex)> <min> <max> <fuzz> "
323                                 + "<flat> [<resolution>]'; expected 5 or 6 numbers but found "
324                                 + parts.length, mReader);
325             }
326             final int axisCode = parseInt(parts[0], 16);
327             final InputAbsInfo info = new InputAbsInfo();
328             info.minimum = parseInt(parts[1], 10);
329             info.maximum = parseInt(parts[2], 10);
330             info.fuzz = parseInt(parts[3], 10);
331             info.flat = parseInt(parts[4], 10);
332             info.resolution = parts.length > 5 ? parseInt(parts[5], 10) : 0;
333             absInfos.append(axisCode, info);
334             line = acceptLine("A");
335         }
336         return absInfos;
337     }
338 
skipUnsupportedLines(String type)339     private void skipUnsupportedLines(String type) throws IOException {
340         if (acceptLine(type) != null) {
341             while (acceptLine(type) != null) {
342                 // Skip the line.
343             }
344         }
345     }
346 
347     /**
348      * Returns the contents of the next line in the file if it has the given type, or raises an
349      * error if it does not.
350      *
351      * @param type the type of the line to expect, represented by the letter before the ':'.
352      * @return the part of the line after the ": ".
353      */
expectLine(String type)354     private String expectLine(String type) throws IOException {
355         final String line = acceptLine(type);
356         if (line == null) {
357             throw new ParsingException("Expected line of type '" + type + "'. (Lines should be in "
358                     + "the order N, I, P, B, A, L, S, E.)",
359                     mReader.peekLine(), mReader.getPreviousLineNumber() + 1);
360         } else {
361             return line;
362         }
363     }
364 
365     /**
366      * Peeks at the next line in the file to see if it has the given type, and if so, returns its
367      * contents and advances the reader.
368      *
369      * @param type the type of the line to accept, represented by the letter before the ':'.
370      * @return the part of the line after the ": ", if the type matches; otherwise {@code null}.
371      */
acceptLine(String type)372     private @Nullable String acceptLine(String type) throws IOException {
373         final String line = mReader.peekLine();
374         if (line == null) {
375             return null;
376         }
377         final String[] lineParts = line.split(": ", 2);
378         if (lineParts.length < 2) {
379             throw new ParsingException("Missing type separator ': '",
380                     line, mReader.getPreviousLineNumber() + 1);
381         }
382         if (lineParts[0].equals(type)) {
383             mReader.advance();
384             return lineParts[1];
385         } else {
386             return null;
387         }
388     }
389 
expectParts(String line, int numParts)390     private String[] expectParts(String line, int numParts) {
391         final String[] parts = line.strip().split(" ");
392         if (parts.length != numParts) {
393             throw new ParsingException(
394                     "Expected a line with " + numParts + " space-separated parts, but found one "
395                             + "with " + parts.length, mReader);
396         }
397         return parts;
398     }
399 
parseInt(String s, int radix)400     private int parseInt(String s, int radix) {
401         try {
402             return Integer.parseInt(s, radix);
403         } catch (NumberFormatException ex) {
404             throw new ParsingException(
405                     "'" + s + "' is not a valid integer of base " + radix, mReader);
406         }
407     }
408 
parseLong(String s, int radix)409     private long parseLong(String s, int radix) {
410         try {
411             return Long.parseLong(s, radix);
412         } catch (NumberFormatException ex) {
413             throw new ParsingException("'" + s + "' is not a valid long of base " + radix, mReader);
414         }
415     }
416 
bitmapToEventCodes(List<Integer> bytes)417     private static int[] bitmapToEventCodes(List<Integer> bytes) {
418         final List<Integer> codes = new ArrayList<>();
419         for (int iByte = 0; iByte < bytes.size(); iByte++) {
420             int b = bytes.get(iByte);
421             for (int iBit = 0; iBit < 8; iBit++) {
422                 if ((b & 1) != 0) {
423                     codes.add(iByte * 8 + iBit);
424                 }
425                 b >>= 1;
426             }
427         }
428         return unboxIntList(codes);
429     }
430 
unboxIntList(List<Integer> list)431     private static int[] unboxIntList(List<Integer> list) {
432         final int[] array = new int[list.size()];
433         Arrays.setAll(array, list::get);
434         return array;
435     }
436 }
437