Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8767c663 authored by Harry Cutts's avatar Harry Cutts
Browse files

uinput: use nanoseconds for delay durations

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
Change-Id: I68d54c05214ae778167adfd0d3fc9a345454c336
parent a0d2cc09
Loading
Loading
Loading
Loading
+27 −9
Original line number Diff line number Diff line
@@ -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");
@@ -101,7 +101,13 @@ public class Device {
        }

        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);
    }

    /**
@@ -112,16 +118,26 @@ public class Device {
    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;
    }

    /**
@@ -131,7 +147,8 @@ public class Device {
     * @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());
    }

    /**
@@ -140,7 +157,8 @@ public class Device {
     */
    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();
+3 −3
Original line number Diff line number Diff line
@@ -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;
@@ -152,7 +152,7 @@ public class EvemuParser implements EventParser {
        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());
    }

@@ -204,7 +204,7 @@ public class EvemuParser implements EventParser {
                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();
            }
        }
+7 −7
Original line number Diff line number Diff line
@@ -100,7 +100,7 @@ public class Event {
    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;
@@ -150,8 +150,8 @@ public class Event {
        return mConfiguration;
    }

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

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

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

        public void setFfEffectsMax(int ffEffectsMax) {
@@ -271,7 +271,7 @@ public class Event {
                    }
                }
                case DELAY -> {
                    if (mEvent.mDurationMillis <= 0) {
                    if (mEvent.mDurationNanos <= 0) {
                        throw new IllegalStateException("Delay has missing or invalid duration");
                    }
                }
+2 −1
Original line number Diff line number Diff line
@@ -71,7 +71,8 @@ public class JsonStyleParser implements EventParser {
                        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();
+1 −1
Original line number Diff line number Diff line
@@ -135,7 +135,7 @@ public class Uinput {
            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());
        }
    }
Loading