Skip to content

Commit

Permalink
Reland "uinput: use nanoseconds for delay durations"
Browse files Browse the repository at this point in the history
(This CL is unchanged. Original description below, with additional Test:
line)

evemu recordings use microseconds for their time intervals, but we can
only schedule handler calls in Device at millisecond precision. So far
we've converted the microseconds into milliseconds in EvemuParser, which
means that the precision losses compound over time (since each delay
will be slightly shorter than it should be, and the next delay will
start from that slightly earlier time, etc.). Keeping the delay
durations in a more precise unit up until the very last moment means
that we'll only get the precision loss once for each event. Since it's
somewhat uncommon to use microseconds elsewhere in Android code, and we
get the system time in nanoseconds, we may as well use nanoseconds
rather than microseconds.

Bug: 310958309
Test: play an evemu recording through uinput
Test: atest UinputTests
Test: atest android.view.cts.input.InputDeviceKeyLayoutMapTest \
            android.view.cts.input.InputDeviceSensorManagerTest \
	    --rerun-until-failure=10
Change-Id: Ibb968487ed114a4c464ac7061af4cda188e92498
  • Loading branch information
HarryCutts committed Feb 12, 2024
1 parent 6d92ca2 commit b8c7f5e
Show file tree
Hide file tree
Showing 6 changed files with 45 additions and 26 deletions.
36 changes: 27 additions & 9 deletions cmds/uinput/src/com/android/commands/uinput/Device.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public class Device {
private final SparseArray<InputAbsInfo> mAbsInfo;
private final OutputStream mOutputStream;
private final Object mCond = new Object();
private long mTimeToSend;
private long mTimeToSendNanos;

static {
System.loadLibrary("uinputcommand_jni");
Expand Down Expand Up @@ -101,7 +101,13 @@ public Device(int id, String name, int vendorId, int productId, int versionId, i
}

mHandler.obtainMessage(MSG_OPEN_UINPUT_DEVICE, args).sendToTarget();
mTimeToSend = SystemClock.uptimeMillis();
mTimeToSendNanos = SystemClock.uptimeNanos();
}

private long getTimeToSendMillis() {
// This should be the same as (long) Math.ceil(mTimeToSendNanos / 1_000_000.0), except
// without the precision loss that comes from converting from long to double and back.
return mTimeToSendNanos / 1_000_000 + ((mTimeToSendNanos % 1_000_000 > 0) ? 1 : 0);
}

/**
Expand All @@ -112,16 +118,26 @@ public Device(int id, String name, int vendorId, int productId, int versionId, i
public void injectEvent(int[] events) {
// if two messages are sent at identical time, they will be processed in order received
Message msg = mHandler.obtainMessage(MSG_INJECT_EVENT, events);
mHandler.sendMessageAtTime(msg, mTimeToSend);
mHandler.sendMessageAtTime(msg, getTimeToSendMillis());
}

/**
* Impose a delay to the device for execution.
* Delay subsequent device activity by the specified amount of time.
*
* <p>Note that although the delay is specified in nanoseconds, due to limitations of {@link
* Handler}'s API, scheduling only occurs with millisecond precision. When scheduling an
* injection or sync, the time at which it is scheduled will be rounded up to the nearest
* millisecond. While this means that a particular injection cannot be scheduled precisely,
* rounding errors will not accumulate over time. For example, if five injections are scheduled
* with a delay of 1,200,000ns before each one, the total delay will be 6ms, as opposed to the
* 10ms it would have been if each individual delay had been rounded up (as {@link EvemuParser}
* would otherwise have to do to avoid sending timestamps that are in the future).
*
* @param delay Time to delay in unit of milliseconds.
* @param delayNanos Time to delay in unit of nanoseconds.
*/
public void addDelay(int delay) {
mTimeToSend = Math.max(SystemClock.uptimeMillis(), mTimeToSend) + delay;
public void addDelayNanos(long delayNanos) {
mTimeToSendNanos =
Math.max(SystemClock.uptimeNanos(), mTimeToSendNanos) + delayNanos;
}

/**
Expand All @@ -131,7 +147,8 @@ public void addDelay(int delay) {
* @param syncToken The token for this sync command.
*/
public void syncEvent(String syncToken) {
mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_SYNC_EVENT, syncToken), mTimeToSend);
mHandler.sendMessageAtTime(
mHandler.obtainMessage(MSG_SYNC_EVENT, syncToken), getTimeToSendMillis());
}

/**
Expand All @@ -140,7 +157,8 @@ public void syncEvent(String syncToken) {
*/
public void close() {
Message msg = mHandler.obtainMessage(MSG_CLOSE_UINPUT_DEVICE);
mHandler.sendMessageAtTime(msg, Math.max(SystemClock.uptimeMillis(), mTimeToSend) + 1);
mHandler.sendMessageAtTime(
msg, Math.max(SystemClock.uptimeMillis(), getTimeToSendMillis()) + 1);
try {
synchronized (mCond) {
mCond.wait();
Expand Down
6 changes: 3 additions & 3 deletions cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class EvemuParser implements EventParser {
* recordings, this will always be the same.
*/
private static final int DEVICE_ID = 1;
private static final int REGISTRATION_DELAY_MILLIS = 500;
private static final int REGISTRATION_DELAY_NANOS = 500_000_000;

private static class CommentAwareReader {
private final LineNumberReader mReader;
Expand Down Expand Up @@ -152,7 +152,7 @@ public EvemuParser(Reader in) throws IOException {
final Event.Builder delayEb = new Event.Builder();
delayEb.setId(DEVICE_ID);
delayEb.setCommand(Event.Command.DELAY);
delayEb.setDurationMillis(REGISTRATION_DELAY_MILLIS);
delayEb.setDurationNanos(REGISTRATION_DELAY_NANOS);
mQueuedEvents.add(delayEb.build());
}

Expand Down Expand Up @@ -204,7 +204,7 @@ public Event getNextEvent() throws IOException {
final Event.Builder delayEb = new Event.Builder();
delayEb.setId(DEVICE_ID);
delayEb.setCommand(Event.Command.DELAY);
delayEb.setDurationMillis((int) (delayMicros / 1000));
delayEb.setDurationNanos(delayMicros * 1000);
return delayEb.build();
}
}
Expand Down
14 changes: 7 additions & 7 deletions cmds/uinput/src/com/android/commands/uinput/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public int getValue() {
private int mBusId;
private int[] mInjections;
private SparseArray<int[]> mConfiguration;
private int mDurationMillis;
private long mDurationNanos;
private int mFfEffectsMax = 0;
private String mInputPort;
private SparseArray<InputAbsInfo> mAbsInfo;
Expand Down Expand Up @@ -150,8 +150,8 @@ public SparseArray<int[]> getConfiguration() {
return mConfiguration;
}

public int getDurationMillis() {
return mDurationMillis;
public long getDurationNanos() {
return mDurationNanos;
}

public int getFfEffectsMax() {
Expand Down Expand Up @@ -182,7 +182,7 @@ public String toString() {
+ ", busId=" + mBusId
+ ", events=" + Arrays.toString(mInjections)
+ ", configuration=" + mConfiguration
+ ", duration=" + mDurationMillis + "ms"
+ ", duration=" + mDurationNanos + "ns"
+ ", ff_effects_max=" + mFfEffectsMax
+ ", port=" + mInputPort
+ "}";
Expand Down Expand Up @@ -237,8 +237,8 @@ public void setBusId(int busId) {
mEvent.mBusId = busId;
}

public void setDurationMillis(int durationMillis) {
mEvent.mDurationMillis = durationMillis;
public void setDurationNanos(long durationNanos) {
mEvent.mDurationNanos = durationNanos;
}

public void setFfEffectsMax(int ffEffectsMax) {
Expand Down Expand Up @@ -271,7 +271,7 @@ public Event build() {
}
}
case DELAY -> {
if (mEvent.mDurationMillis <= 0) {
if (mEvent.mDurationNanos <= 0) {
throw new IllegalStateException("Delay has missing or invalid duration");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ public Event getNextEvent() throws IOException {
case "configuration" -> eb.setConfiguration(readConfiguration());
case "ff_effects_max" -> eb.setFfEffectsMax(readInt());
case "abs_info" -> eb.setAbsInfo(readAbsInfoArray());
case "duration" -> eb.setDurationMillis(readInt());
// Duration is specified in milliseconds in the JSON-style format.
case "duration" -> eb.setDurationNanos(readInt() * 1_000_000L);
case "port" -> eb.setInputPort(mReader.nextString());
case "syncToken" -> eb.setSyncToken(mReader.nextString());
default -> mReader.skipValue();
Expand Down
2 changes: 1 addition & 1 deletion cmds/uinput/src/com/android/commands/uinput/Uinput.java
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ private void process(Event e) {
case REGISTER ->
error("Device id=" + e.getId() + " is already registered. Ignoring event.");
case INJECT -> d.injectEvent(e.getInjections());
case DELAY -> d.addDelay(e.getDurationMillis());
case DELAY -> d.addDelayNanos(e.getDurationNanos());
case SYNC -> d.syncEvent(e.getSyncToken());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,10 @@ private void assertInjectEvent(Event event, int eventType, int eventCode, int va
.containsExactly(eventType, eventCode, value).inOrder();
}

private void assertDelayEvent(Event event, int durationMillis) {
private void assertDelayEvent(Event event, int durationMicros) {
assertThat(event).isNotNull();
assertThat(event.getCommand()).isEqualTo(Event.Command.DELAY);
assertThat(event.getDurationMillis()).isEqualTo(durationMillis);
assertThat(event.getDurationMicros()).isEqualTo(durationMicros);
}

@Test
Expand Down Expand Up @@ -231,12 +231,12 @@ public void testEventParsing_MultipleFrames() throws IOException {
assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1);
assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0);

assertDelayEvent(parser.getNextEvent(), 10);
assertDelayEvent(parser.getNextEvent(), 10000);

assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 0);
assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0);

assertDelayEvent(parser.getNextEvent(), 1000);
assertDelayEvent(parser.getNextEvent(), 1000000);

assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1);
assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0);
Expand Down Expand Up @@ -490,7 +490,7 @@ public void testFreeDesktopEvemuRecording() throws IOException {
assertInjectEvent(parser.getNextEvent(), 0x3, 0x18, 56);
assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0);

assertDelayEvent(parser.getNextEvent(), 6);
assertDelayEvent(parser.getNextEvent(), 6080);

assertInjectEvent(parser.getNextEvent(), 0x3, 0x0035, 888);
}
Expand Down

0 comments on commit b8c7f5e

Please sign in to comment.