Add multiple device resampling support to InputConsumerNoResampling with tests

Added multiple device resampling support to InputConsumerNoResampling
with unit tests to ensure correctness

Bug: 297226446
Flag: EXEMPT refactor
Test: TEST=libinput_tests; m $TEST && $ANDROID_HOST_OUT/nativetest64/$TEST/$TEST --gtest_filter="InputConsumerTest*"
Change-Id: I45528a89e0b60f46b0095078356382ed701b191b
diff --git a/libs/input/InputConsumerNoResampling.cpp b/libs/input/InputConsumerNoResampling.cpp
index cdbc186..ce8bb43 100644
--- a/libs/input/InputConsumerNoResampling.cpp
+++ b/libs/input/InputConsumerNoResampling.cpp
@@ -17,8 +17,6 @@
 #define LOG_TAG "InputConsumerNoResampling"
 #define ATRACE_TAG ATRACE_TAG_INPUT
 
-#include <chrono>
-
 #include <inttypes.h>
 
 #include <android-base/logging.h>
@@ -39,6 +37,8 @@
 
 using std::chrono::nanoseconds;
 
+using android::base::Result;
+
 /**
  * Log debug messages relating to the consumer end of the transport channel.
  * Enable this via "adb shell setprop log.tag.InputTransportConsumer DEBUG" (requires restart)
@@ -169,24 +169,18 @@
     msg.body.timeline.graphicsTimeline[GraphicsTimeline::PRESENT_TIME] = presentTime;
     return msg;
 }
-
-bool isPointerEvent(const MotionEvent& motionEvent) {
-    return (motionEvent.getSource() & AINPUT_SOURCE_CLASS_POINTER) == AINPUT_SOURCE_CLASS_POINTER;
-}
 } // namespace
 
-using android::base::Result;
-
 // --- InputConsumerNoResampling ---
 
-InputConsumerNoResampling::InputConsumerNoResampling(const std::shared_ptr<InputChannel>& channel,
-                                                     sp<Looper> looper,
-                                                     InputConsumerCallbacks& callbacks,
-                                                     std::unique_ptr<Resampler> resampler)
+InputConsumerNoResampling::InputConsumerNoResampling(
+        const std::shared_ptr<InputChannel>& channel, sp<Looper> looper,
+        InputConsumerCallbacks& callbacks,
+        std::function<std::unique_ptr<Resampler>()> resamplerCreator)
       : mChannel{channel},
         mLooper{looper},
         mCallbacks{callbacks},
-        mResampler{std::move(resampler)},
+        mResamplerCreator{std::move(resamplerCreator)},
         mFdEvents(0) {
     LOG_ALWAYS_FATAL_IF(mLooper == nullptr);
     mCallback = sp<LooperEventCallback>::make(
@@ -319,7 +313,6 @@
 }
 
 void InputConsumerNoResampling::handleMessages(std::vector<InputMessage>&& messages) {
-    // TODO(b/297226446) : add resampling
     for (const InputMessage& msg : messages) {
         if (msg.header.type == InputMessage::Type::MOTION) {
             const int32_t action = msg.body.motion.action;
@@ -329,12 +322,31 @@
                                          action == AMOTION_EVENT_ACTION_HOVER_MOVE) &&
                     (isFromSource(source, AINPUT_SOURCE_CLASS_POINTER) ||
                      isFromSource(source, AINPUT_SOURCE_CLASS_JOYSTICK));
+
+            const bool canResample = (mResamplerCreator != nullptr) &&
+                    (isFromSource(source, AINPUT_SOURCE_CLASS_POINTER));
+            if (canResample) {
+                if (action == AMOTION_EVENT_ACTION_DOWN) {
+                    if (std::unique_ptr<Resampler> resampler = mResamplerCreator();
+                        resampler != nullptr) {
+                        const auto [_, inserted] =
+                                mResamplers.insert(std::pair(deviceId, std::move(resampler)));
+                        LOG_IF(WARNING, !inserted) << deviceId << "already exists in mResamplers";
+                    }
+                }
+            }
+
             if (batchableEvent) {
                 // add it to batch
                 mBatches[deviceId].emplace(msg);
             } else {
                 // consume all pending batches for this device immediately
                 consumeBatchedInputEvents(deviceId, /*requestedFrameTime=*/std::nullopt);
+                if (canResample &&
+                    (action == AMOTION_EVENT_ACTION_UP || action == AMOTION_EVENT_ACTION_CANCEL)) {
+                    LOG_IF(INFO, mResamplers.erase(deviceId) == 0)
+                            << deviceId << "does not exist in mResamplers";
+                }
                 handleMessage(msg);
             }
         } else {
@@ -456,8 +468,13 @@
                                                     std::queue<InputMessage>& messages) {
     std::unique_ptr<MotionEvent> motionEvent;
     std::optional<uint32_t> firstSeqForBatch;
-    const nanoseconds resampleLatency =
-            (mResampler != nullptr) ? mResampler->getResampleLatency() : nanoseconds{0};
+
+    LOG_IF(FATAL, messages.empty()) << "messages queue is empty!";
+    const DeviceId deviceId = messages.front().body.motion.deviceId;
+    const auto resampler = mResamplers.find(deviceId);
+    const nanoseconds resampleLatency = (resampler != mResamplers.cend())
+            ? resampler->second->getResampleLatency()
+            : nanoseconds{0};
     const nanoseconds adjustedFrameTime = nanoseconds{requestedFrameTime} - resampleLatency;
 
     while (!messages.empty() &&
@@ -474,15 +491,17 @@
         }
         messages.pop();
     }
+
     // Check if resampling should be performed.
-    if (motionEvent != nullptr && isPointerEvent(*motionEvent) && mResampler != nullptr) {
-        InputMessage* futureSample = nullptr;
-        if (!messages.empty()) {
-            futureSample = &messages.front();
-        }
-        mResampler->resampleMotionEvent(nanoseconds{requestedFrameTime}, *motionEvent,
-                                        futureSample);
+    InputMessage* futureSample = nullptr;
+    if (!messages.empty()) {
+        futureSample = &messages.front();
     }
+    if ((motionEvent != nullptr) && (resampler != mResamplers.cend())) {
+        resampler->second->resampleMotionEvent(nanoseconds{requestedFrameTime}, *motionEvent,
+                                               futureSample);
+    }
+
     return std::make_pair(std::move(motionEvent), firstSeqForBatch);
 }
 
diff --git a/libs/input/Resampler.cpp b/libs/input/Resampler.cpp
index 51fadf8..328fa68 100644
--- a/libs/input/Resampler.cpp
+++ b/libs/input/Resampler.cpp
@@ -247,11 +247,6 @@
 
 void LegacyResampler::resampleMotionEvent(nanoseconds frameTime, MotionEvent& motionEvent,
                                           const InputMessage* futureSample) {
-    if (mPreviousDeviceId && *mPreviousDeviceId != motionEvent.getDeviceId()) {
-        mLatestSamples.clear();
-    }
-    mPreviousDeviceId = motionEvent.getDeviceId();
-
     const nanoseconds resampleTime = frameTime - RESAMPLE_LATENCY;
 
     updateLatestSamples(motionEvent);
diff --git a/libs/input/tests/InputConsumer_test.cpp b/libs/input/tests/InputConsumer_test.cpp
index d708316..cbb332e 100644
--- a/libs/input/tests/InputConsumer_test.cpp
+++ b/libs/input/tests/InputConsumer_test.cpp
@@ -16,6 +16,9 @@
 
 #include <input/InputConsumerNoResampling.h>
 
+#include <gtest/gtest.h>
+
+#include <chrono>
 #include <memory>
 #include <optional>
 
@@ -25,7 +28,9 @@
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
 #include <input/BlockingQueue.h>
+#include <input/Input.h>
 #include <input/InputEventBuilders.h>
+#include <input/Resampler.h>
 #include <utils/Looper.h>
 #include <utils/StrongPointer.h>
 
@@ -37,8 +42,18 @@
 
 using ::testing::AllOf;
 using ::testing::Matcher;
-using ::testing::Not;
 
+struct Pointer {
+    int32_t id{0};
+    ToolType toolType{ToolType::FINGER};
+    float x{0.0f};
+    float y{0.0f};
+    bool isResampled{false};
+
+    PointerBuilder asPointerBuilder() const {
+        return PointerBuilder{id, toolType}.x(x).y(y).isResampled(isResampled);
+    }
+};
 } // namespace
 
 class InputConsumerTest : public testing::Test, public InputConsumerCallbacks {
@@ -47,9 +62,9 @@
           : mClientTestChannel{std::make_shared<TestInputChannel>("TestChannel")},
             mLooper{sp<Looper>::make(/*allowNonCallbacks=*/false)} {
         Looper::setForThread(mLooper);
-        mConsumer =
-                std::make_unique<InputConsumerNoResampling>(mClientTestChannel, mLooper, *this,
-                                                            std::make_unique<LegacyResampler>());
+        mConsumer = std::make_unique<
+                InputConsumerNoResampling>(mClientTestChannel, mLooper, *this,
+                                           []() { return std::make_unique<LegacyResampler>(); });
     }
 
     void invokeLooperCallback() const {
@@ -71,6 +86,9 @@
         EXPECT_THAT(*motionEvent, matcher);
     }
 
+    InputMessage nextPointerMessage(std::chrono::nanoseconds eventTime, DeviceId deviceId,
+                                    int32_t action, const Pointer& pointer);
+
     std::shared_ptr<TestInputChannel> mClientTestChannel;
     sp<Looper> mLooper;
     std::unique_ptr<InputConsumerNoResampling> mConsumer;
@@ -83,6 +101,7 @@
     BlockingQueue<std::unique_ptr<TouchModeEvent>> mTouchModeEvents;
 
 private:
+    uint32_t mLastSeq{0};
     size_t mOnBatchedInputEventPendingInvocationCount{0};
 
     // InputConsumerCallbacks interface
@@ -118,6 +137,19 @@
     };
 };
 
+InputMessage InputConsumerTest::nextPointerMessage(std::chrono::nanoseconds eventTime,
+                                                   DeviceId deviceId, int32_t action,
+                                                   const Pointer& pointer) {
+    ++mLastSeq;
+    return InputMessageBuilder{InputMessage::Type::MOTION, mLastSeq}
+            .eventTime(eventTime.count())
+            .deviceId(deviceId)
+            .source(AINPUT_SOURCE_TOUCHSCREEN)
+            .action(action)
+            .pointer(pointer.asPointerBuilder())
+            .build();
+}
+
 TEST_F(InputConsumerTest, MessageStreamBatchedInMotionEvent) {
     mClientTestChannel->enqueueMessage(InputMessageBuilder{InputMessage::Type::MOTION, /*seq=*/0}
                                                .eventTime(nanoseconds{0ms}.count())
@@ -235,8 +267,7 @@
                                                .build());
 
     invokeLooperCallback();
-    assertReceivedMotionEvent(AllOf(WithDeviceId(0), WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
-                                    Not(MotionEventIsResampled())));
+    assertReceivedMotionEvent(AllOf(WithDeviceId(0), WithMotionAction(AMOTION_EVENT_ACTION_MOVE)));
 
     mClientTestChannel->assertFinishMessage(/*seq=*/0, /*handled=*/true);
     mClientTestChannel->assertFinishMessage(/*seq=*/4, /*handled=*/true);
@@ -244,4 +275,121 @@
     mClientTestChannel->assertFinishMessage(/*seq=*/2, /*handled=*/true);
     mClientTestChannel->assertFinishMessage(/*seq=*/3, /*handled=*/true);
 }
+
+/**
+ * The test supposes a 60Hz Vsync rate and a 200Hz input rate. The InputMessages are intertwined as
+ * in a real use cases. The test's two devices should be resampled independently. Moreover, the
+ * InputMessage stream layout for the test is:
+ *
+ * DOWN(0, 0ms)
+ * MOVE(0, 5ms)
+ * MOVE(0, 10ms)
+ * DOWN(1, 15ms)
+ *
+ * CONSUME(16ms)
+ *
+ * MOVE(1, 20ms)
+ * MOVE(1, 25ms)
+ * MOVE(0, 30ms)
+ *
+ * CONSUME(32ms)
+ *
+ * MOVE(0, 35ms)
+ * UP(1, 40ms)
+ * UP(0, 45ms)
+ *
+ * CONSUME(48ms)
+ *
+ * The first field is device ID, and the second field is event time.
+ */
+TEST_F(InputConsumerTest, MultiDeviceResampling) {
+    mClientTestChannel->enqueueMessage(nextPointerMessage(0ms, /*deviceId=*/0,
+                                                          AMOTION_EVENT_ACTION_DOWN,
+                                                          Pointer{.x = 0, .y = 0}));
+
+    mClientTestChannel->assertNoSentMessages();
+
+    invokeLooperCallback();
+    assertReceivedMotionEvent(AllOf(WithDeviceId(0), WithMotionAction(AMOTION_EVENT_ACTION_DOWN),
+                                    WithSampleCount(1)));
+
+    mClientTestChannel->enqueueMessage(nextPointerMessage(5ms, /*deviceId=*/0,
+                                                          AMOTION_EVENT_ACTION_MOVE,
+                                                          Pointer{.x = 1.0f, .y = 2.0f}));
+    mClientTestChannel->enqueueMessage(nextPointerMessage(10ms, /*deviceId=*/0,
+                                                          AMOTION_EVENT_ACTION_MOVE,
+                                                          Pointer{.x = 2.0f, .y = 4.0f}));
+    mClientTestChannel->enqueueMessage(nextPointerMessage(15ms, /*deviceId=*/1,
+                                                          AMOTION_EVENT_ACTION_DOWN,
+                                                          Pointer{.x = 10.0f, .y = 10.0f}));
+
+    invokeLooperCallback();
+    mConsumer->consumeBatchedInputEvents(16'000'000 /*ns*/);
+
+    assertReceivedMotionEvent(AllOf(WithDeviceId(1), WithMotionAction(AMOTION_EVENT_ACTION_DOWN),
+                                    WithSampleCount(1)));
+    assertReceivedMotionEvent(
+            AllOf(WithDeviceId(0), WithMotionAction(AMOTION_EVENT_ACTION_MOVE), WithSampleCount(3),
+                  WithSample(/*sampleIndex=*/2,
+                             Sample{11ms,
+                                    {PointerArgs{.x = 2.2f, .y = 4.4f, .isResampled = true}}})));
+
+    mClientTestChannel->enqueueMessage(nextPointerMessage(20ms, /*deviceId=*/1,
+                                                          AMOTION_EVENT_ACTION_MOVE,
+                                                          Pointer{.x = 11.0f, .y = 12.0f}));
+    mClientTestChannel->enqueueMessage(nextPointerMessage(25ms, /*deviceId=*/1,
+                                                          AMOTION_EVENT_ACTION_MOVE,
+                                                          Pointer{.x = 12.0f, .y = 14.0f}));
+    mClientTestChannel->enqueueMessage(nextPointerMessage(30ms, /*deviceId=*/0,
+                                                          AMOTION_EVENT_ACTION_MOVE,
+                                                          Pointer{.x = 5.0f, .y = 6.0f}));
+
+    invokeLooperCallback();
+    assertOnBatchedInputEventPendingWasCalled();
+    mConsumer->consumeBatchedInputEvents(32'000'000 /*ns*/);
+
+    assertReceivedMotionEvent(
+            AllOf(WithDeviceId(1), WithMotionAction(AMOTION_EVENT_ACTION_MOVE), WithSampleCount(3),
+                  WithSample(/*sampleIndex=*/2,
+                             Sample{27ms,
+                                    {PointerArgs{.x = 12.4f, .y = 14.8f, .isResampled = true}}})));
+
+    mClientTestChannel->enqueueMessage(nextPointerMessage(35ms, /*deviceId=*/0,
+                                                          AMOTION_EVENT_ACTION_MOVE,
+                                                          Pointer{.x = 8.0f, .y = 9.0f}));
+    mClientTestChannel->enqueueMessage(nextPointerMessage(40ms, /*deviceId=*/1,
+                                                          AMOTION_EVENT_ACTION_UP,
+                                                          Pointer{.x = 12.0f, .y = 14.0f}));
+    mClientTestChannel->enqueueMessage(nextPointerMessage(45ms, /*deviceId=*/0,
+                                                          AMOTION_EVENT_ACTION_UP,
+                                                          Pointer{.x = 8.0f, .y = 9.0f}));
+
+    invokeLooperCallback();
+    mConsumer->consumeBatchedInputEvents(48'000'000 /*ns*/);
+
+    assertReceivedMotionEvent(
+            AllOf(WithDeviceId(1), WithMotionAction(AMOTION_EVENT_ACTION_UP), WithSampleCount(1)));
+
+    assertReceivedMotionEvent(
+            AllOf(WithDeviceId(0), WithMotionAction(AMOTION_EVENT_ACTION_MOVE), WithSampleCount(3),
+                  WithSample(/*sampleIndex=*/2,
+                             Sample{37'500'000ns,
+                                    {PointerArgs{.x = 9.5f, .y = 10.5f, .isResampled = true}}})));
+
+    assertReceivedMotionEvent(
+            AllOf(WithDeviceId(0), WithMotionAction(AMOTION_EVENT_ACTION_UP), WithSampleCount(1)));
+
+    // The sequence order is based on the expected consumption. Each sequence number corresponds to
+    // one of the previously enqueued messages.
+    mClientTestChannel->assertFinishMessage(/*seq=*/1, /*handled=*/true);
+    mClientTestChannel->assertFinishMessage(/*seq=*/4, /*handled=*/true);
+    mClientTestChannel->assertFinishMessage(/*seq=*/2, /*handled=*/true);
+    mClientTestChannel->assertFinishMessage(/*seq=*/3, /*handled=*/true);
+    mClientTestChannel->assertFinishMessage(/*seq=*/5, /*handled=*/true);
+    mClientTestChannel->assertFinishMessage(/*seq=*/6, /*handled=*/true);
+    mClientTestChannel->assertFinishMessage(/*seq=*/9, /*handled=*/true);
+    mClientTestChannel->assertFinishMessage(/*seq=*/7, /*handled=*/true);
+    mClientTestChannel->assertFinishMessage(/*seq=*/8, /*handled=*/true);
+    mClientTestChannel->assertFinishMessage(/*seq=*/10, /*handled=*/true);
+}
 } // namespace android
diff --git a/libs/input/tests/Resampler_test.cpp b/libs/input/tests/Resampler_test.cpp
index 26dee39..fae8518 100644
--- a/libs/input/tests/Resampler_test.cpp
+++ b/libs/input/tests/Resampler_test.cpp
@@ -87,7 +87,6 @@
 struct InputStream {
     std::vector<InputSample> samples{};
     int32_t action{0};
-    DeviceId deviceId{0};
     /**
      * Converts from InputStream to MotionEvent. Enables calling LegacyResampler methods only with
      * the relevant data for tests.
@@ -100,8 +99,8 @@
     MotionEventBuilder motionEventBuilder =
             MotionEventBuilder(action, AINPUT_SOURCE_CLASS_POINTER)
                     .downTime(0)
-                    .eventTime(static_cast<std::chrono::nanoseconds>(firstSample.eventTime).count())
-                    .deviceId(deviceId);
+                    .eventTime(
+                            static_cast<std::chrono::nanoseconds>(firstSample.eventTime).count());
     for (const Pointer& pointer : firstSample.pointers) {
         const PointerBuilder pointerBuilder =
                 PointerBuilder(pointer.id, pointer.toolType).x(pointer.x).y(pointer.y);
@@ -289,28 +288,6 @@
     assertMotionEventIsNotResampled(originalMotionEvent, motionEvent);
 }
 
-TEST_F(ResamplerTest, SinglePointerDifferentDeviceIdBetweenMotionEvents) {
-    MotionEvent motionFromFirstDevice =
-            InputStream{{InputSample{4ms, {{.id = 0, .x = 1.0f, .y = 1.0f, .isResampled = false}}},
-                         InputSample{8ms, {{.id = 0, .x = 2.0f, .y = 2.0f, .isResampled = false}}}},
-                        AMOTION_EVENT_ACTION_MOVE,
-                        .deviceId = 0};
-
-    mResampler->resampleMotionEvent(10ms, motionFromFirstDevice, nullptr);
-
-    MotionEvent motionFromSecondDevice =
-            InputStream{{InputSample{11ms,
-                                     {{.id = 0, .x = 3.0f, .y = 3.0f, .isResampled = false}}}},
-                        AMOTION_EVENT_ACTION_MOVE,
-                        .deviceId = 1};
-    const MotionEvent originalMotionEvent = motionFromSecondDevice;
-
-    mResampler->resampleMotionEvent(12ms, motionFromSecondDevice, nullptr);
-    // The MotionEvent should not be resampled because the second event came from a different device
-    // than the previous event.
-    assertMotionEventIsNotResampled(originalMotionEvent, motionFromSecondDevice);
-}
-
 TEST_F(ResamplerTest, SinglePointerSingleSampleInterpolation) {
     MotionEvent motionEvent =
             InputStream{{InputSample{10ms,
diff --git a/libs/input/tests/TestEventMatchers.h b/libs/input/tests/TestEventMatchers.h
index dd2e40c..3589de5 100644
--- a/libs/input/tests/TestEventMatchers.h
+++ b/libs/input/tests/TestEventMatchers.h
@@ -16,18 +16,39 @@
 
 #pragma once
 
+#include <chrono>
 #include <ostream>
+#include <vector>
 
+#include <android-base/logging.h>
+#include <gtest/gtest.h>
 #include <input/Input.h>
 
 namespace android {
 
+namespace {
+
+using ::testing::Matcher;
+
+} // namespace
+
 /**
  * This file contains a copy of Matchers from .../inputflinger/tests/TestEventMatchers.h. Ideally,
  * implementations must not be duplicated.
  * TODO(b/365606513): Find a way to share TestEventMatchers.h between inputflinger and libinput.
  */
 
+struct PointerArgs {
+    float x{0.0f};
+    float y{0.0f};
+    bool isResampled{false};
+};
+
+struct Sample {
+    std::chrono::nanoseconds eventTime{0};
+    std::vector<PointerArgs> pointers{};
+};
+
 class WithDeviceIdMatcher {
 public:
     using is_gtest_matcher = void;
@@ -79,32 +100,88 @@
     return WithMotionActionMatcher(action);
 }
 
-class MotionEventIsResampledMatcher {
+class WithSampleCountMatcher {
 public:
     using is_gtest_matcher = void;
+    explicit WithSampleCountMatcher(size_t sampleCount) : mExpectedSampleCount{sampleCount} {}
 
     bool MatchAndExplain(const MotionEvent& motionEvent, std::ostream*) const {
-        const size_t numSamples = motionEvent.getHistorySize() + 1;
-        const size_t numPointers = motionEvent.getPointerCount();
-        if (numPointers <= 0 || numSamples <= 0) {
+        return (motionEvent.getHistorySize() + 1) == mExpectedSampleCount;
+    }
+
+    void DescribeTo(std::ostream* os) const { *os << "sample count " << mExpectedSampleCount; }
+
+    void DescribeNegationTo(std::ostream* os) const { *os << "different sample count"; }
+
+private:
+    const size_t mExpectedSampleCount;
+};
+
+inline WithSampleCountMatcher WithSampleCount(size_t sampleCount) {
+    return WithSampleCountMatcher(sampleCount);
+}
+
+class WithSampleMatcher {
+public:
+    using is_gtest_matcher = void;
+    explicit WithSampleMatcher(size_t sampleIndex, const Sample& sample)
+          : mSampleIndex{sampleIndex}, mSample{sample} {}
+
+    bool MatchAndExplain(const MotionEvent& motionEvent, std::ostream* os) const {
+        if (motionEvent.getHistorySize() < mSampleIndex) {
+            *os << "sample index out of bounds";
             return false;
         }
-        for (size_t i = 0; i < numPointers; ++i) {
+
+        if (motionEvent.getHistoricalEventTime(mSampleIndex) != mSample.eventTime.count()) {
+            *os << "event time mismatch. sample: "
+                << motionEvent.getHistoricalEventTime(mSampleIndex)
+                << " expected: " << mSample.eventTime.count();
+            return false;
+        }
+
+        if (motionEvent.getPointerCount() != mSample.pointers.size()) {
+            *os << "pointer count mismatch. sample: " << motionEvent.getPointerCount()
+                << " expected: " << mSample.pointers.size();
+            return false;
+        }
+
+        for (size_t pointerIndex = 0; pointerIndex < motionEvent.getPointerCount();
+             ++pointerIndex) {
             const PointerCoords& pointerCoords =
-                    motionEvent.getSamplePointerCoords()[numSamples * numPointers + i];
-            if (!pointerCoords.isResampled) {
+                    *(motionEvent.getHistoricalRawPointerCoords(pointerIndex, mSampleIndex));
+            if ((pointerCoords.getX() != mSample.pointers[pointerIndex].x) ||
+                (pointerCoords.getY() != mSample.pointers[pointerIndex].y)) {
+                *os << "sample coordinates mismatch at pointer index " << pointerIndex
+                    << ". sample: (" << pointerCoords.getX() << ", " << pointerCoords.getY()
+                    << ") expected: (" << mSample.pointers[pointerIndex].x << ", "
+                    << mSample.pointers[pointerIndex].y << ")";
+                return false;
+            }
+            if (motionEvent.isResampled(pointerIndex, mSampleIndex) !=
+                mSample.pointers[pointerIndex].isResampled) {
+                *os << "resampling flag mismatch. sample: "
+                    << motionEvent.isResampled(pointerIndex, mSampleIndex)
+                    << " expected: " << mSample.pointers[pointerIndex].isResampled;
                 return false;
             }
         }
         return true;
     }
 
-    void DescribeTo(std::ostream* os) const { *os << "MotionEvent is resampled."; }
+    void DescribeTo(std::ostream* os) const { *os << "motion event sample properties match."; }
 
-    void DescribeNegationTo(std::ostream* os) const { *os << "MotionEvent is not resampled."; }
+    void DescribeNegationTo(std::ostream* os) const {
+        *os << "motion event sample properties do not match expected properties.";
+    }
+
+private:
+    const size_t mSampleIndex;
+    const Sample mSample;
 };
 
-inline MotionEventIsResampledMatcher MotionEventIsResampled() {
-    return MotionEventIsResampledMatcher();
+inline WithSampleMatcher WithSample(size_t sampleIndex, const Sample& sample) {
+    return WithSampleMatcher(sampleIndex, sample);
 }
+
 } // namespace android