Skip to content

Commit

Permalink
Merge changes from topic "revert-26209003-revert-26154399-CXYXSETNUA-…
Browse files Browse the repository at this point in the history
…YFUIACDDDP" into main

* changes:
  Reland "uinput: delay from the end of the last delay"
  Reland "uinput: Specify timestamps when injecting events..."
  Reland "uinput: use nanoseconds for delay durations"
  • Loading branch information
HarryCutts authored and Android (Google) Code Review committed Feb 13, 2024
2 parents 61873aa + dfc798e commit e4b1478
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 75 deletions.
3 changes: 2 additions & 1 deletion cmds/uinput/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ will be unregistered. There is no explicit command for unregistering a device.

#### `delay`

Add a delay to command processing
Add a delay between the processing of commands. The delay will be timed from when the last delay
ended, rather than from the current time, to allow for more precise timings to be produced.

| Field | Type | Description |
|:-------------:|:-------------:|:-------------------------- |
Expand Down
18 changes: 9 additions & 9 deletions cmds/uinput/jni/com_android_commands_uinput_Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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!");
}
Expand Down Expand Up @@ -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)},
Expand Down
12 changes: 7 additions & 5 deletions cmds/uinput/jni/com_android_commands_uinput_Device.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -53,7 +54,8 @@ class UinputDevice {

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:
Expand Down
106 changes: 84 additions & 22 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 All @@ -65,7 +65,8 @@ private static native long nativeOpenUinputDevice(String name, int id, int vendo
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);
Expand Down Expand Up @@ -101,27 +102,54 @@ 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() {
// 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);
}

/**
* 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);
mHandler.sendMessageAtTime(msg, mTimeToSend);
SomeArgs args = SomeArgs.obtain();
args.arg1 = events;
args.argl1 = offsetMicros;
args.argl2 = SystemClock.uptimeNanos();
Message msg = mHandler.obtainMessage(MSG_INJECT_EVENT, args);
mHandler.sendMessageAtTime(msg, getTimeToSendMillis());
}

/**
* Impose a delay to the device for execution.
* Delay subsequent device activity by the specified amount of time.
*
* @param delay Time to delay in unit of milliseconds.
* <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 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 += delayNanos;
}

/**
Expand All @@ -131,7 +159,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 +169,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 All @@ -151,6 +181,7 @@ public void close() {

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

DeviceHandler(Looper looper) {
Expand All @@ -160,7 +191,7 @@ private class DeviceHandler extends Handler {
@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 */,
Expand All @@ -177,15 +208,43 @@ public void handleMessage(Message msg) {
}
args.recycle();
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 at which we scheduled this first
// batch, 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();
Expand All @@ -198,11 +257,14 @@ public void handleMessage(Message msg) {
mCond.notify();
}
break;
case MSG_SYNC_EVENT:
}
case MSG_SYNC_EVENT: {
handleSyncEvent((String) msg.obj);
break;
default:
}
default: {
throw new IllegalArgumentException("Unknown device message");
}
}
}

Expand Down
28 changes: 12 additions & 16 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 All @@ -175,7 +175,6 @@ public Event getNextEvent() throws IOException {
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();
Expand All @@ -192,21 +191,18 @@ public Event getNextEvent() throws IOException {
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;
final Event.Builder delayEb = new Event.Builder();
delayEb.setId(DEVICE_ID);
delayEb.setCommand(Event.Command.DELAY);
delayEb.setDurationMillis((int) (delayMicros / 1000));
return delayEb.build();
}
// Send a delay now, and queue the actual event for the next call.
mQueuedEvents.add(eb.build());
mLastEventTimeMicros = timeMicros;
final Event.Builder delayEb = new Event.Builder();
delayEb.setId(DEVICE_ID);
delayEb.setCommand(Event.Command.DELAY);
delayEb.setDurationNanos(delayMicros * 1000);
return delayEb.build();
}
}

Expand Down
Loading

0 comments on commit e4b1478

Please sign in to comment.