Add touch resampling native tests
Add native tests for touch resampling.
These test the resampling logic in InputTransport.cpp by
sending a pre-determined set of InputEvents via InputPublisher
and read the events back from InputConsumer. The events that are
read back are compared to pre-determined set of events.
Bug: 35412046
Bug: 68840121
Bug: 119214052
Test: atest libinput_tests
Change-Id: I596a6528b8ab27a68d0e9baafa3a8cb6b62d0422
diff --git a/libs/input/InputTransport.cpp b/libs/input/InputTransport.cpp
index 6195052..8d8433b 100644
--- a/libs/input/InputTransport.cpp
+++ b/libs/input/InputTransport.cpp
@@ -51,7 +51,7 @@
// Latency added during resampling. A few milliseconds doesn't hurt much but
// reduces the impact of mispredicted touch positions.
-static const nsecs_t RESAMPLE_LATENCY = 5 * NANOS_PER_MS;
+const std::chrono::duration RESAMPLE_LATENCY = 5ms;
// Minimum time difference between consecutive samples before attempting to resample.
static const nsecs_t RESAMPLE_MIN_DELTA = 2 * NANOS_PER_MS;
@@ -721,7 +721,11 @@
// --- InputConsumer ---
InputConsumer::InputConsumer(const std::shared_ptr<InputChannel>& channel)
- : mResampleTouch(isTouchResamplingEnabled()), mChannel(channel), mMsgDeferred(false) {}
+ : InputConsumer(channel, isTouchResamplingEnabled()) {}
+
+InputConsumer::InputConsumer(const std::shared_ptr<InputChannel>& channel,
+ bool enableTouchResampling)
+ : mResampleTouch(enableTouchResampling), mChannel(channel), mMsgDeferred(false) {}
InputConsumer::~InputConsumer() {
}
@@ -751,7 +755,10 @@
// Receive a fresh message.
status_t result = mChannel->receiveMessage(&mMsg);
if (result == OK) {
- mConsumeTimes.emplace(mMsg.header.seq, systemTime(SYSTEM_TIME_MONOTONIC));
+ const auto [_, inserted] =
+ mConsumeTimes.emplace(mMsg.header.seq, systemTime(SYSTEM_TIME_MONOTONIC));
+ LOG_ALWAYS_FATAL_IF(!inserted, "Already have a consume time for seq=%" PRIu32,
+ mMsg.header.seq);
}
if (result) {
// Consume the next batched event unless batches are being held for later.
@@ -918,7 +925,7 @@
nsecs_t sampleTime = frameTime;
if (mResampleTouch) {
- sampleTime -= RESAMPLE_LATENCY;
+ sampleTime -= std::chrono::nanoseconds(RESAMPLE_LATENCY).count();
}
ssize_t split = findSampleNoLaterThan(batch, sampleTime);
if (split < 0) {
@@ -1166,6 +1173,11 @@
return;
}
+ if (current->eventTime == sampleTime) {
+ // Prevents having 2 events with identical times and coordinates.
+ return;
+ }
+
// Resample touch coordinates.
History oldLastResample;
oldLastResample.initializeFrom(touchState.lastResample);
diff --git a/libs/input/tests/Android.bp b/libs/input/tests/Android.bp
index 6ffe851..54f7586 100644
--- a/libs/input/tests/Android.bp
+++ b/libs/input/tests/Android.bp
@@ -16,6 +16,7 @@
"InputDevice_test.cpp",
"InputEvent_test.cpp",
"InputPublisherAndConsumer_test.cpp",
+ "TouchResampling_test.cpp",
"TouchVideoFrame_test.cpp",
"VelocityTracker_test.cpp",
"VerifiedInputEvent_test.cpp",
diff --git a/libs/input/tests/InputPublisherAndConsumer_test.cpp b/libs/input/tests/InputPublisherAndConsumer_test.cpp
index 05bc0bc..70e4fda 100644
--- a/libs/input/tests/InputPublisherAndConsumer_test.cpp
+++ b/libs/input/tests/InputPublisherAndConsumer_test.cpp
@@ -16,17 +16,10 @@
#include "TestHelpers.h"
-#include <unistd.h>
-#include <sys/mman.h>
-#include <time.h>
-
#include <attestation/HmacKeyManager.h>
-#include <cutils/ashmem.h>
#include <gtest/gtest.h>
#include <gui/constants.h>
#include <input/InputTransport.h>
-#include <utils/StopWatch.h>
-#include <utils/Timers.h>
using android::base::Result;
diff --git a/libs/input/tests/TouchResampling_test.cpp b/libs/input/tests/TouchResampling_test.cpp
new file mode 100644
index 0000000..c09a8e9
--- /dev/null
+++ b/libs/input/tests/TouchResampling_test.cpp
@@ -0,0 +1,562 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "TestHelpers.h"
+
+#include <chrono>
+#include <vector>
+
+#include <attestation/HmacKeyManager.h>
+#include <gtest/gtest.h>
+#include <input/InputTransport.h>
+
+using namespace std::chrono_literals;
+
+namespace android {
+
+struct Pointer {
+ int32_t id;
+ float x;
+ float y;
+};
+
+struct InputEventEntry {
+ std::chrono::nanoseconds eventTime;
+ std::vector<Pointer> pointers;
+ int32_t action;
+};
+
+class TouchResamplingTest : public testing::Test {
+protected:
+ std::unique_ptr<InputPublisher> mPublisher;
+ std::unique_ptr<InputConsumer> mConsumer;
+ PreallocatedInputEventFactory mEventFactory;
+
+ uint32_t mSeq = 1;
+
+ void SetUp() override {
+ std::unique_ptr<InputChannel> serverChannel, clientChannel;
+ status_t result =
+ InputChannel::openInputChannelPair("channel name", serverChannel, clientChannel);
+ ASSERT_EQ(OK, result);
+
+ mPublisher = std::make_unique<InputPublisher>(std::move(serverChannel));
+ mConsumer = std::make_unique<InputConsumer>(std::move(clientChannel),
+ true /* enableTouchResampling */);
+ }
+
+ status_t publishSimpleMotionEventWithCoords(int32_t action, nsecs_t eventTime,
+ const std::vector<PointerProperties>& properties,
+ const std::vector<PointerCoords>& coords);
+ void publishSimpleMotionEvent(int32_t action, nsecs_t eventTime,
+ const std::vector<Pointer>& pointers);
+ void publishInputEventEntries(const std::vector<InputEventEntry>& entries);
+ void consumeInputEventEntries(const std::vector<InputEventEntry>& entries,
+ std::chrono::nanoseconds frameTime);
+ void receiveResponseUntilSequence(uint32_t seq);
+};
+
+status_t TouchResamplingTest::publishSimpleMotionEventWithCoords(
+ int32_t action, nsecs_t eventTime, const std::vector<PointerProperties>& properties,
+ const std::vector<PointerCoords>& coords) {
+ const ui::Transform identityTransform;
+ const nsecs_t downTime = 0;
+
+ if (action == AMOTION_EVENT_ACTION_DOWN && eventTime != 0) {
+ ADD_FAILURE() << "Downtime should be equal to 0 (hardcoded for convenience)";
+ }
+ return mPublisher->publishMotionEvent(mSeq++, InputEvent::nextId(), 1 /*deviceId*/,
+ AINPUT_SOURCE_TOUCHSCREEN, 0 /*displayId*/, INVALID_HMAC,
+ action, 0 /*actionButton*/, 0 /*flags*/, 0 /*edgeFlags*/,
+ AMETA_NONE, 0 /*buttonState*/, MotionClassification::NONE,
+ identityTransform, 0 /*xPrecision*/, 0 /*yPrecision*/,
+ AMOTION_EVENT_INVALID_CURSOR_POSITION,
+ AMOTION_EVENT_INVALID_CURSOR_POSITION, identityTransform,
+ downTime, eventTime, properties.size(), properties.data(),
+ coords.data());
+}
+
+void TouchResamplingTest::publishSimpleMotionEvent(int32_t action, nsecs_t eventTime,
+ const std::vector<Pointer>& pointers) {
+ std::vector<PointerProperties> properties;
+ std::vector<PointerCoords> coords;
+
+ for (const Pointer& pointer : pointers) {
+ properties.push_back({});
+ properties.back().clear();
+ properties.back().id = pointer.id;
+ properties.back().toolType = AMOTION_EVENT_TOOL_TYPE_FINGER;
+
+ coords.push_back({});
+ coords.back().clear();
+ coords.back().setAxisValue(AMOTION_EVENT_AXIS_X, pointer.x);
+ coords.back().setAxisValue(AMOTION_EVENT_AXIS_Y, pointer.y);
+ }
+
+ status_t result = publishSimpleMotionEventWithCoords(action, eventTime, properties, coords);
+ ASSERT_EQ(OK, result);
+}
+
+/**
+ * Each entry is published separately, one entry at a time. As a result, action is used here
+ * on a per-entry basis.
+ */
+void TouchResamplingTest::publishInputEventEntries(const std::vector<InputEventEntry>& entries) {
+ for (const InputEventEntry& entry : entries) {
+ publishSimpleMotionEvent(entry.action, entry.eventTime.count(), entry.pointers);
+ }
+}
+
+/**
+ * Inside the publisher, read responses repeatedly until the desired sequence number is returned.
+ *
+ * Sometimes, when you call 'sendFinishedSignal', you would be finishing a batch which is comprised
+ * of several input events. As a result, consumer will generate multiple 'finish' signals on your
+ * behalf.
+ *
+ * In this function, we call 'receiveConsumerResponse' in a loop until the desired sequence number
+ * is returned.
+ */
+void TouchResamplingTest::receiveResponseUntilSequence(uint32_t seq) {
+ size_t consumedEvents = 0;
+ while (consumedEvents < 100) {
+ android::base::Result<InputPublisher::ConsumerResponse> response =
+ mPublisher->receiveConsumerResponse();
+ ASSERT_TRUE(response.ok());
+ ASSERT_TRUE(std::holds_alternative<InputPublisher::Finished>(*response));
+ const InputPublisher::Finished& finish = std::get<InputPublisher::Finished>(*response);
+ ASSERT_TRUE(finish.handled)
+ << "publisher receiveFinishedSignal should have set handled to consumer's reply";
+ if (finish.seq == seq) {
+ return;
+ }
+ consumedEvents++;
+ }
+ FAIL() << "Got " << consumedEvents << "events, but still no event with seq=" << seq;
+}
+
+/**
+ * All entries are compared against a single MotionEvent, but the same data structure
+ * InputEventEntry is used here for simpler code. As a result, the entire array of InputEventEntry
+ * must contain identical values for the action field.
+ */
+void TouchResamplingTest::consumeInputEventEntries(const std::vector<InputEventEntry>& entries,
+ std::chrono::nanoseconds frameTime) {
+ ASSERT_GE(entries.size(), 1U) << "Must have at least 1 InputEventEntry to compare against";
+
+ uint32_t consumeSeq;
+ InputEvent* event;
+
+ status_t status = mConsumer->consume(&mEventFactory, true /*consumeBatches*/, frameTime.count(),
+ &consumeSeq, &event);
+ ASSERT_EQ(OK, status);
+ MotionEvent* motionEvent = static_cast<MotionEvent*>(event);
+
+ ASSERT_EQ(entries.size() - 1, motionEvent->getHistorySize());
+ for (size_t i = 0; i < entries.size(); i++) { // most recent sample is last
+ SCOPED_TRACE(i);
+ const InputEventEntry& entry = entries[i];
+ ASSERT_EQ(entry.action, motionEvent->getAction());
+ ASSERT_EQ(entry.eventTime.count(), motionEvent->getHistoricalEventTime(i));
+ ASSERT_EQ(entry.pointers.size(), motionEvent->getPointerCount());
+
+ for (size_t p = 0; p < motionEvent->getPointerCount(); p++) {
+ SCOPED_TRACE(p);
+ // The pointers can be in any order, both in MotionEvent as well as InputEventEntry
+ ssize_t motionEventPointerIndex = motionEvent->findPointerIndex(entry.pointers[p].id);
+ ASSERT_GE(motionEventPointerIndex, 0) << "Pointer must be present in MotionEvent";
+ ASSERT_EQ(entry.pointers[p].x,
+ motionEvent->getHistoricalAxisValue(AMOTION_EVENT_AXIS_X,
+ motionEventPointerIndex, i));
+ ASSERT_EQ(entry.pointers[p].x,
+ motionEvent->getHistoricalRawAxisValue(AMOTION_EVENT_AXIS_X,
+ motionEventPointerIndex, i));
+ ASSERT_EQ(entry.pointers[p].y,
+ motionEvent->getHistoricalAxisValue(AMOTION_EVENT_AXIS_Y,
+ motionEventPointerIndex, i));
+ ASSERT_EQ(entry.pointers[p].y,
+ motionEvent->getHistoricalRawAxisValue(AMOTION_EVENT_AXIS_Y,
+ motionEventPointerIndex, i));
+ }
+ }
+
+ status = mConsumer->sendFinishedSignal(consumeSeq, true);
+ ASSERT_EQ(OK, status);
+
+ receiveResponseUntilSequence(consumeSeq);
+}
+
+/**
+ * Timeline
+ * ---------+------------------+------------------+--------+-----------------+----------------------
+ * 0 ms 10 ms 20 ms 25 ms 35 ms
+ * ACTION_DOWN ACTION_MOVE ACTION_MOVE ^ ^
+ * | |
+ * resampled value |
+ * frameTime
+ * Typically, the prediction is made for time frameTime - RESAMPLE_LATENCY, or 30 ms in this case
+ * However, that would be 10 ms later than the last real sample (which came in at 20 ms).
+ * Therefore, the resampling should happen at 20 ms + RESAMPLE_MAX_PREDICTION = 28 ms.
+ * In this situation, though, resample time is further limited by taking half of the difference
+ * between the last two real events, which would put this time at:
+ * 20 ms + (20 ms - 10 ms) / 2 = 25 ms.
+ */
+TEST_F(TouchResamplingTest, EventIsResampled) {
+ std::chrono::nanoseconds frameTime;
+ std::vector<InputEventEntry> entries, expectedEntries;
+
+ // Initial ACTION_DOWN should be separate, because the first consume event will only return
+ // InputEvent with a single action.
+ entries = {
+ // id x y
+ {0ms, {{0, 10, 20}}, AMOTION_EVENT_ACTION_DOWN},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 5ms;
+ expectedEntries = {
+ // id x y
+ {0ms, {{0, 10, 20}}, AMOTION_EVENT_ACTION_DOWN},
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+
+ // Two ACTION_MOVE events 10 ms apart that move in X direction and stay still in Y
+ entries = {
+ // id x y
+ {10ms, {{0, 20, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ {20ms, {{0, 30, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 35ms;
+ expectedEntries = {
+ // id x y
+ {10ms, {{0, 20, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ {20ms, {{0, 30, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ {25ms, {{0, 35, 30}}, AMOTION_EVENT_ACTION_MOVE}, // resampled value
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+}
+
+/**
+ * Same as above test, but use pointer id=1 instead of 0 to make sure that system does not
+ * have these hardcoded.
+ */
+TEST_F(TouchResamplingTest, EventIsResampledWithDifferentId) {
+ std::chrono::nanoseconds frameTime;
+ std::vector<InputEventEntry> entries, expectedEntries;
+
+ // Initial ACTION_DOWN should be separate, because the first consume event will only return
+ // InputEvent with a single action.
+ entries = {
+ // id x y
+ {0ms, {{1, 10, 20}}, AMOTION_EVENT_ACTION_DOWN},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 5ms;
+ expectedEntries = {
+ // id x y
+ {0ms, {{1, 10, 20}}, AMOTION_EVENT_ACTION_DOWN},
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+
+ // Two ACTION_MOVE events 10 ms apart that move in X direction and stay still in Y
+ entries = {
+ // id x y
+ {10ms, {{1, 20, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ {20ms, {{1, 30, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 35ms;
+ expectedEntries = {
+ // id x y
+ {10ms, {{1, 20, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ {20ms, {{1, 30, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ {25ms, {{1, 35, 30}}, AMOTION_EVENT_ACTION_MOVE}, // resampled value
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+}
+
+/**
+ * Event should not be resampled when sample time is equal to event time.
+ */
+TEST_F(TouchResamplingTest, SampleTimeEqualsEventTime) {
+ std::chrono::nanoseconds frameTime;
+ std::vector<InputEventEntry> entries, expectedEntries;
+
+ // Initial ACTION_DOWN should be separate, because the first consume event will only return
+ // InputEvent with a single action.
+ entries = {
+ // id x y
+ {0ms, {{0, 10, 20}}, AMOTION_EVENT_ACTION_DOWN},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 5ms;
+ expectedEntries = {
+ // id x y
+ {0ms, {{0, 10, 20}}, AMOTION_EVENT_ACTION_DOWN},
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+
+ // Two ACTION_MOVE events 10 ms apart that move in X direction and stay still in Y
+ entries = {
+ // id x y
+ {10ms, {{0, 20, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ {20ms, {{0, 30, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 20ms + 5ms /*RESAMPLE_LATENCY*/;
+ expectedEntries = {
+ // id x y
+ {10ms, {{0, 20, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ {20ms, {{0, 30, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ // no resampled event because the time of resample falls exactly on the existing event
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+}
+
+/**
+ * Once we send a resampled value to the app, we should continue to "lie" if the pointer
+ * does not move. So, if the pointer keeps the same coordinates, resampled value should continue
+ * to be used.
+ */
+TEST_F(TouchResamplingTest, ResampledValueIsUsedForIdenticalCoordinates) {
+ std::chrono::nanoseconds frameTime;
+ std::vector<InputEventEntry> entries, expectedEntries;
+
+ // Initial ACTION_DOWN should be separate, because the first consume event will only return
+ // InputEvent with a single action.
+ entries = {
+ // id x y
+ {0ms, {{0, 10, 20}}, AMOTION_EVENT_ACTION_DOWN},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 5ms;
+ expectedEntries = {
+ // id x y
+ {0ms, {{0, 10, 20}}, AMOTION_EVENT_ACTION_DOWN},
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+
+ // Two ACTION_MOVE events 10 ms apart that move in X direction and stay still in Y
+ entries = {
+ // id x y
+ {10ms, {{0, 20, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ {20ms, {{0, 30, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 35ms;
+ expectedEntries = {
+ // id x y
+ {10ms, {{0, 20, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ {20ms, {{0, 30, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ {25ms, {{0, 35, 30}}, AMOTION_EVENT_ACTION_MOVE}, // resampled value
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+
+ // Coordinate value 30 has been resampled to 35. When a new event comes in with value 30 again,
+ // the system should still report 35.
+ entries = {
+ // id x y
+ {40ms, {{0, 30, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 45ms + 5ms /*RESAMPLE_LATENCY*/;
+ expectedEntries = {
+ // id x y
+ {40ms, {{0, 35, 30}}, AMOTION_EVENT_ACTION_MOVE}, // original event, rewritten
+ {45ms, {{0, 35, 30}}, AMOTION_EVENT_ACTION_MOVE}, // resampled event, rewritten
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+}
+
+TEST_F(TouchResamplingTest, OldEventReceivedAfterResampleOccurs) {
+ std::chrono::nanoseconds frameTime;
+ std::vector<InputEventEntry> entries, expectedEntries;
+
+ // Initial ACTION_DOWN should be separate, because the first consume event will only return
+ // InputEvent with a single action.
+ entries = {
+ // id x y
+ {0ms, {{0, 10, 20}}, AMOTION_EVENT_ACTION_DOWN},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 5ms;
+ expectedEntries = {
+ // id x y
+ {0ms, {{0, 10, 20}}, AMOTION_EVENT_ACTION_DOWN},
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+
+ // Two ACTION_MOVE events 10 ms apart that move in X direction and stay still in Y
+ entries = {
+ // id x y
+ {10ms, {{0, 20, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ {20ms, {{0, 30, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 35ms;
+ expectedEntries = {
+ // id x y
+ {10ms, {{0, 20, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ {20ms, {{0, 30, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ {25ms, {{0, 35, 30}}, AMOTION_EVENT_ACTION_MOVE}, // resampled value
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+ // Above, the resampled event is at 25ms rather than at 30 ms = 35ms - RESAMPLE_LATENCY
+ // because we are further bound by how far we can extrapolate by the "last time delta".
+ // That's 50% of (20 ms - 10ms) => 5ms. So we can't predict more than 5 ms into the future
+ // from the event at 20ms, which is why the resampled event is at t = 25 ms.
+
+ // We resampled the event to 25 ms. Now, an older 'real' event comes in.
+ entries = {
+ // id x y
+ {24ms, {{0, 40, 30}}, AMOTION_EVENT_ACTION_MOVE},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 50ms;
+ expectedEntries = {
+ // id x y
+ {24ms, {{0, 35, 30}}, AMOTION_EVENT_ACTION_MOVE}, // original event, rewritten
+ {26ms, {{0, 45, 30}}, AMOTION_EVENT_ACTION_MOVE}, // resampled event, rewritten
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+}
+
+TEST_F(TouchResamplingTest, TwoPointersAreResampledIndependently) {
+ std::chrono::nanoseconds frameTime;
+ std::vector<InputEventEntry> entries, expectedEntries;
+
+ // full action for when a pointer with id=1 appears (some other pointer must already be present)
+ constexpr int32_t actionPointer1Down =
+ AMOTION_EVENT_ACTION_POINTER_DOWN + (1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);
+
+ // full action for when a pointer with id=0 disappears (some other pointer must still remain)
+ constexpr int32_t actionPointer0Up =
+ AMOTION_EVENT_ACTION_POINTER_UP + (0 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);
+
+ // Initial ACTION_DOWN should be separate, because the first consume event will only return
+ // InputEvent with a single action.
+ entries = {
+ // id x y
+ {0ms, {{0, 100, 100}}, AMOTION_EVENT_ACTION_DOWN},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 5ms;
+ expectedEntries = {
+ // id x y
+ {0ms, {{0, 100, 100}}, AMOTION_EVENT_ACTION_DOWN},
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+
+ entries = {
+ // id x y
+ {10ms, {{0, 100, 100}}, AMOTION_EVENT_ACTION_MOVE},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 10ms + 5ms /*RESAMPLE_LATENCY*/;
+ expectedEntries = {
+ // id x y
+ {10ms, {{0, 100, 100}}, AMOTION_EVENT_ACTION_MOVE},
+ // no resampled value because frameTime - RESAMPLE_LATENCY == eventTime
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+
+ // Second pointer id=1 appears
+ entries = {
+ // id x y
+ {15ms, {{0, 100, 100}, {1, 500, 500}}, actionPointer1Down},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 20ms + 5ms /*RESAMPLE_LATENCY*/;
+ expectedEntries = {
+ // id x y
+ {15ms, {{0, 100, 100}, {1, 500, 500}}, actionPointer1Down},
+ // no resampled value because frameTime - RESAMPLE_LATENCY == eventTime
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+
+ // Both pointers move
+ entries = {
+ // id x y
+ {30ms, {{0, 100, 100}, {1, 500, 500}}, AMOTION_EVENT_ACTION_MOVE},
+ {40ms, {{0, 120, 120}, {1, 600, 600}}, AMOTION_EVENT_ACTION_MOVE},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 45ms + 5ms /*RESAMPLE_LATENCY*/;
+ expectedEntries = {
+ // id x y
+ {30ms, {{0, 100, 100}, {1, 500, 500}}, AMOTION_EVENT_ACTION_MOVE},
+ {40ms, {{0, 120, 120}, {1, 600, 600}}, AMOTION_EVENT_ACTION_MOVE},
+ {45ms, {{0, 130, 130}, {1, 650, 650}}, AMOTION_EVENT_ACTION_MOVE}, // resampled value
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+
+ // Both pointers move again
+ entries = {
+ // id x y
+ {60ms, {{0, 120, 120}, {1, 600, 600}}, AMOTION_EVENT_ACTION_MOVE},
+ {70ms, {{0, 130, 130}, {1, 700, 700}}, AMOTION_EVENT_ACTION_MOVE},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 75ms + 5ms /*RESAMPLE_LATENCY*/;
+ /**
+ * The sample at t = 60, pointer id 0 is not equal to 120, because this value of 120 was
+ * received twice, and resampled to 130. So if we already reported it as "130", we continue
+ * to report it as such. Similar with pointer id 1.
+ */
+ expectedEntries = {
+ {60ms,
+ {{0, 130, 130}, // not 120! because it matches previous real event
+ {1, 650, 650}},
+ AMOTION_EVENT_ACTION_MOVE},
+ {70ms, {{0, 130, 130}, {1, 700, 700}}, AMOTION_EVENT_ACTION_MOVE},
+ {75ms, {{0, 135, 135}, {1, 750, 750}}, AMOTION_EVENT_ACTION_MOVE}, // resampled value
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+
+ // First pointer id=0 leaves the screen
+ entries = {
+ // id x y
+ {80ms, {{1, 600, 600}}, actionPointer0Up},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 90ms;
+ expectedEntries = {
+ // id x y
+ {80ms, {{1, 600, 600}}, actionPointer0Up},
+ // no resampled event for ACTION_POINTER_UP
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+
+ // Remaining pointer id=1 is still present, but doesn't move
+ entries = {
+ // id x y
+ {90ms, {{1, 600, 600}}, AMOTION_EVENT_ACTION_MOVE},
+ };
+ publishInputEventEntries(entries);
+ frameTime = 100ms;
+ expectedEntries = {
+ // id x y
+ {90ms, {{1, 600, 600}}, AMOTION_EVENT_ACTION_MOVE},
+ /**
+ * The latest event with ACTION_MOVE was at t = 70, coord = 700.
+ * Use that value for resampling here: (600 - 700) / (90 - 70) * 5 + 600
+ */
+ {95ms, {{1, 575, 575}}, AMOTION_EVENT_ACTION_MOVE}, // resampled value
+ };
+ consumeInputEventEntries(expectedEntries, frameTime);
+}
+
+} // namespace android