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