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

Commit ee829890 authored by Harry Cutts's avatar Harry Cutts
Browse files

uinput: Specify timestamps when injecting events from evemu

Last year support for specifying timestamps for uinput events was added
to the Linux Kernel [0]. This allows us to give precise timestamps for
events being played back from an evemu recording, despite the actual
times at which we inject them being imprecise due to how threads and
the kernel are scheduled.

[0]: https://lore.kernel.org/lkml/20230427000152.1407471-1-biswarupp@google.com/

Bug: 310958309
Test: with the patch added to the device's kernel, play back pointing
      stick and touchscreen recordings. Compare `getevent -lt` output
      with the timestamps in the recordings to check that the offsets
      between timestamps match (e.g. using a spreadsheet).
Test: atest UinputTests
Change-Id: If486cdb7218918aca64e6561f9fc2f30acce736a
parent 8767c663
Loading
Loading
Loading
Loading
+9 −9
Original line number Diff line number Diff line
@@ -166,14 +166,14 @@ UinputDevice::~UinputDevice() {
    ::ioctl(mFd, UI_DEV_DESTROY);
}

void UinputDevice::injectEvent(uint16_t type, uint16_t code, int32_t value) {
void UinputDevice::injectEvent(std::chrono::microseconds timestamp, uint16_t type, uint16_t code,
                               int32_t value) {
    struct input_event event = {};
    event.type = type;
    event.code = code;
    event.value = value;
    timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    TIMESPEC_TO_TIMEVAL(&event.time, &ts);
    event.time.tv_sec = timestamp.count() / 1'000'000;
    event.time.tv_usec = timestamp.count() % 1'000'000;

    if (::write(mFd, &event, sizeof(input_event)) < 0) {
        ALOGE("Could not write event %" PRIu16 " %" PRIu16 " with value %" PRId32 " : %s", type,
@@ -268,12 +268,12 @@ static void closeUinputDevice(JNIEnv* /* env */, jclass /* clazz */, jlong ptr)
    }
}

static void injectEvent(JNIEnv* /* env */, jclass /* clazz */, jlong ptr, jint type, jint code,
                        jint value) {
static void injectEvent(JNIEnv* /* env */, jclass /* clazz */, jlong ptr, jlong timestampMicros,
                        jint type, jint code, jint value) {
    uinput::UinputDevice* d = reinterpret_cast<uinput::UinputDevice*>(ptr);
    if (d != nullptr) {
        d->injectEvent(static_cast<uint16_t>(type), static_cast<uint16_t>(code),
                       static_cast<int32_t>(value));
        d->injectEvent(std::chrono::microseconds(timestampMicros), static_cast<uint16_t>(type),
                       static_cast<uint16_t>(code), static_cast<int32_t>(value));
    } else {
        ALOGE("Could not inject event, Device* is null!");
    }
@@ -330,7 +330,7 @@ static JNINativeMethod sMethods[] = {
         "(Ljava/lang/String;IIIIIILjava/lang/String;"
         "Lcom/android/commands/uinput/Device$DeviceCallback;)J",
         reinterpret_cast<void*>(openUinputDevice)},
        {"nativeInjectEvent", "(JIII)V", reinterpret_cast<void*>(injectEvent)},
        {"nativeInjectEvent", "(JJIII)V", reinterpret_cast<void*>(injectEvent)},
        {"nativeConfigure", "(II[I)V", reinterpret_cast<void*>(configure)},
        {"nativeSetAbsInfo", "(IILandroid/os/Parcel;)V", reinterpret_cast<void*>(setAbsInfo)},
        {"nativeCloseUinputDevice", "(J)V", reinterpret_cast<void*>(closeUinputDevice)},
+7 −5
Original line number Diff line number Diff line
@@ -14,13 +14,14 @@
 * limitations under the License.
 */

#include <memory>
#include <vector>

#include <android-base/unique_fd.h>
#include <jni.h>
#include <linux/input.h>

#include <android-base/unique_fd.h>
#include <chrono>
#include <memory>
#include <vector>

#include "src/com/android/commands/uinput/InputAbsInfo.h"

namespace android {
@@ -53,7 +54,8 @@ public:

    virtual ~UinputDevice();

    void injectEvent(uint16_t type, uint16_t code, int32_t value);
    void injectEvent(std::chrono::microseconds timestamp, uint16_t type, uint16_t code,
                     int32_t value);
    int handleEvents(int events);

private:
+59 −13
Original line number Diff line number Diff line
@@ -65,7 +65,8 @@ public class Device {
            int productId, int versionId, int bus, int ffEffectsMax, String port,
            DeviceCallback callback);
    private static native void nativeCloseUinputDevice(long ptr);
    private static native void nativeInjectEvent(long ptr, int type, int code, int value);
    private static native void nativeInjectEvent(long ptr, long timestampMicros, int type, int code,
                                                 int value);
    private static native void nativeConfigure(int handle, int code, int[] configs);
    private static native void nativeSetAbsInfo(int handle, int axisCode, Parcel axisParcel);
    private static native int nativeGetEvdevEventTypeByLabel(String label);
@@ -105,6 +106,11 @@ public class Device {
    }

    private long getTimeToSendMillis() {
        // Since we can only specify delays in milliseconds but evemu timestamps are in
        // microseconds, we have to round up the delays to avoid setting event timestamps
        // which are in the future (which the kernel would silently reject and replace with
        // the current time).
        //
        // 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);
@@ -114,10 +120,17 @@ public class Device {
     * Inject uinput events to device
     *
     * @param events  Array of raw uinput events.
     * @param offsetMicros The difference in microseconds between the timestamps of the previous
     *                     batch of events injected and this batch. If set to -1, the current
     *                     timestamp will be used.
     */
    public void injectEvent(int[] events) {
    public void injectEvent(int[] events, long offsetMicros) {
        // if two messages are sent at identical time, they will be processed in order received
        Message msg = mHandler.obtainMessage(MSG_INJECT_EVENT, events);
        SomeArgs args = SomeArgs.obtain();
        args.arg1 = events;
        args.argl1 = offsetMicros;
        args.argl2 = mTimeToSendNanos;
        Message msg = mHandler.obtainMessage(MSG_INJECT_EVENT, args);
        mHandler.sendMessageAtTime(msg, getTimeToSendMillis());
    }

@@ -169,6 +182,7 @@ public class Device {

    private class DeviceHandler extends Handler {
        private long mPtr;
        private long mLastInjectTimestampMicros = -1;
        private int mBarrierToken;

        DeviceHandler(Looper looper) {
@@ -178,7 +192,7 @@ public class Device {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_OPEN_UINPUT_DEVICE:
                case MSG_OPEN_UINPUT_DEVICE: {
                    SomeArgs args = (SomeArgs) msg.obj;
                    String name = (String) args.arg1;
                    mPtr = nativeOpenUinputDevice(name, args.argi1 /* id */,
@@ -193,15 +207,44 @@ public class Device {
                        throw ex;
                    }
                    break;
                case MSG_INJECT_EVENT:
                    if (mPtr != 0) {
                        int[] events = (int[]) msg.obj;
                        for (int pos = 0; pos + 2 < events.length; pos += 3) {
                            nativeInjectEvent(mPtr, events[pos], events[pos + 1], events[pos + 2]);
                }
                case MSG_INJECT_EVENT: {
                    SomeArgs args = (SomeArgs) msg.obj;
                    if (mPtr == 0) {
                        args.recycle();
                        break;
                    }
                    long offsetMicros = args.argl1;
                    if (mLastInjectTimestampMicros == -1 || offsetMicros == -1) {
                        // There's often a delay of a few milliseconds between the time specified to
                        // Handler.sendMessageAtTime and the handler actually being called, due to
                        // the way threads are scheduled. We don't take this into account when
                        // calling addDelayNanos between the first batch of event injections (when
                        // we set the "base timestamp" from which all others will be offset) and the
                        // second batch, meaning that the actual time between the handler calls for
                        // those batches may be less than the offset between their timestamps. When
                        // that happens, we would pass a timestamp for the second batch that's
                        // actually in the future. The kernel's uinput API rejects timestamps that
                        // are in the future and uses the current time instead, making the reported
                        // timestamps inconsistent with the recording we're replaying.
                        //
                        // To prevent this, we need to use the time we scheduled this first batch
                        // for (in microseconds, to avoid potential rounding up from
                        // getTimeToSendMillis), rather than the actual current time.
                        mLastInjectTimestampMicros = args.argl2 / 1000;
                    } else {
                        mLastInjectTimestampMicros += offsetMicros;
                    }

                    int[] events = (int[]) args.arg1;
                    for (int pos = 0; pos + 2 < events.length; pos += 3) {
                        nativeInjectEvent(mPtr, mLastInjectTimestampMicros, events[pos],
                                events[pos + 1], events[pos + 2]);
                    }
                    args.recycle();
                    break;
                case MSG_CLOSE_UINPUT_DEVICE:
                }
                case MSG_CLOSE_UINPUT_DEVICE: {
                    if (mPtr != 0) {
                        nativeCloseUinputDevice(mPtr);
                        getLooper().quitSafely();
@@ -214,13 +257,16 @@ public class Device {
                        mCond.notify();
                    }
                    break;
                case MSG_SYNC_EVENT:
                }
                case MSG_SYNC_EVENT: {
                    handleSyncEvent((String) msg.obj);
                    break;
                default:
                }
                default: {
                    throw new IllegalArgumentException("Unknown device message");
                }
            }
        }

        public void pauseEvents() {
            mBarrierToken = getLooper().myQueue().postSyncBarrier();
+10 −14
Original line number Diff line number Diff line
@@ -175,7 +175,6 @@ public class EvemuParser implements EventParser {
            throw new ParsingException(
                    "Invalid timestamp '" + parts[0] + "' (should contain a single '.')", mReader);
        }
        // TODO(b/310958309): use timeMicros to set the timestamp on the event being sent.
        final long timeMicros =
                parseLong(timeParts[0], 10) * 1_000_000 + parseInt(timeParts[1], 10);
        final Event.Builder eb = new Event.Builder();
@@ -192,12 +191,10 @@ public class EvemuParser implements EventParser {
            return eb.build();
        } else {
            final long delayMicros = timeMicros - mLastEventTimeMicros;
            // The shortest delay supported by Handler.sendMessageAtTime (used for timings by the
            // Device class) is 1ms, so ignore time differences smaller than that.
            if (delayMicros < 1000) {
                mLastEventTimeMicros = timeMicros;
            eb.setTimestampOffsetMicros(delayMicros);
            if (delayMicros == 0) {
                return eb.build();
            } else {
            }
            // Send a delay now, and queue the actual event for the next call.
            mQueuedEvents.add(eb.build());
            mLastEventTimeMicros = timeMicros;
@@ -208,7 +205,6 @@ public class EvemuParser implements EventParser {
            return delayEb.build();
        }
    }
    }

    private Event parseRegistrationEvent() throws IOException {
        // The registration details at the start of a recording are specified by a set of lines
+15 −1
Original line number Diff line number Diff line
@@ -99,6 +99,7 @@ public class Event {
    private int mVersionId;
    private int mBusId;
    private int[] mInjections;
    private long mTimestampOffsetMicros = -1;
    private SparseArray<int[]> mConfiguration;
    private long mDurationNanos;
    private int mFfEffectsMax = 0;
@@ -138,12 +139,21 @@ public class Event {
        return mInjections;
    }

    /**
     * Returns the number of microseconds that should be added to the previous {@code INJECT}
     * event's timestamp to produce the timestamp for this {@code INJECT} event. A value of -1
     * indicates that the current timestamp should be used instead.
     */
    public long getTimestampOffsetMicros() {
        return mTimestampOffsetMicros;
    }

    /**
     * Returns a {@link SparseArray} describing the event codes that should be registered for the
     * device. The keys are uinput ioctl codes (such as those returned from {@link
     * UinputControlCode#getValue()}, while the values are arrays of event codes to be enabled with
     * those ioctls. For example, key 101 (corresponding to {@link UinputControlCode#UI_SET_KEYBIT})
     * could have values 0x110 ({@code BTN_LEFT}, 0x111 ({@code BTN_RIGHT}), and 0x112
     * could have values 0x110 ({@code BTN_LEFT}), 0x111 ({@code BTN_RIGHT}), and 0x112
     * ({@code BTN_MIDDLE}).
     */
    public SparseArray<int[]> getConfiguration() {
@@ -211,6 +221,10 @@ public class Event {
            mEvent.mInjections = events;
        }

        public void setTimestampOffsetMicros(long offsetMicros) {
            mEvent.mTimestampOffsetMicros = offsetMicros;
        }

        /**
         * Sets the event codes that should be registered with a {@code register} command.
         *
Loading