Merge changes Iebd90276,I645481db,I2c00fca0,I289e4470

* changes:
  TouchInputMapper: s/mExternalStylusId/mFusedStylusPointerId
  Ensure stylus buttons generate events consistently
  Add integration tests to verify the behavior of a fused external stylus
  Add unit tests for external stylus fusion
diff --git a/services/inputflinger/reader/mapper/TouchInputMapper.cpp b/services/inputflinger/reader/mapper/TouchInputMapper.cpp
index bf73ce5..8bae650 100644
--- a/services/inputflinger/reader/mapper/TouchInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/TouchInputMapper.cpp
@@ -21,6 +21,7 @@
 #include "TouchInputMapper.h"
 
 #include <ftl/enum.h>
+#include <input/PrintTools.h>
 
 #include "CursorButtonAccumulator.h"
 #include "CursorScrollAccumulator.h"
@@ -31,13 +32,6 @@
 
 // --- Constants ---
 
-// Maximum amount of latency to add to touch events while waiting for data from an
-// external stylus.
-static constexpr nsecs_t EXTERNAL_STYLUS_DATA_TIMEOUT = ms2ns(72);
-
-// Maximum amount of time to wait on touch data before pushing out new pressure data.
-static constexpr nsecs_t TOUCH_DATA_TIMEOUT = ms2ns(20);
-
 // Artificial latency on synthetic events created from stylus data without corresponding touch
 // data.
 static constexpr nsecs_t STYLUS_DATA_LATENCY = ms2ns(10);
@@ -258,9 +252,12 @@
     dump += INDENT3 "Stylus Fusion:\n";
     dump += StringPrintf(INDENT4 "ExternalStylusConnected: %s\n",
                          toString(mExternalStylusConnected));
-    dump += StringPrintf(INDENT4 "External Stylus ID: %" PRId64 "\n", mExternalStylusId);
+    dump += StringPrintf(INDENT4 "Fused External Stylus Pointer ID: %s\n",
+                         toString(mFusedStylusPointerId).c_str());
     dump += StringPrintf(INDENT4 "External Stylus Data Timeout: %" PRId64 "\n",
                          mExternalStylusFusionTimeout);
+    dump += StringPrintf(INDENT4 " External Stylus Buttons Applied: 0x%08x",
+                         mExternalStylusButtonsApplied);
     dump += INDENT3 "External Stylus State:\n";
     dumpStylusState(dump, mExternalStylusState);
 
@@ -1417,9 +1414,10 @@
 
 void TouchInputMapper::resetExternalStylus() {
     mExternalStylusState.clear();
-    mExternalStylusId = -1;
+    mFusedStylusPointerId.reset();
     mExternalStylusFusionTimeout = LLONG_MAX;
     mExternalStylusDataPending = false;
+    mExternalStylusButtonsApplied = 0;
 }
 
 void TouchInputMapper::clearStylusDataPendingFlags() {
@@ -1693,8 +1691,17 @@
 }
 
 void TouchInputMapper::applyExternalStylusButtonState(nsecs_t when) {
-    if (mDeviceMode == DeviceMode::DIRECT && hasExternalStylus() && mExternalStylusId != -1) {
-        mCurrentRawState.buttonState |= mExternalStylusState.buttons;
+    if (mDeviceMode == DeviceMode::DIRECT && hasExternalStylus()) {
+        // If any of the external buttons are already pressed by the touch device, ignore them.
+        const int32_t pressedButtons = ~mCurrentRawState.buttonState & mExternalStylusState.buttons;
+        const int32_t releasedButtons =
+                mExternalStylusButtonsApplied & ~mExternalStylusState.buttons;
+
+        mCurrentRawState.buttonState |= pressedButtons;
+        mCurrentRawState.buttonState &= ~releasedButtons;
+
+        mExternalStylusButtonsApplied |= pressedButtons;
+        mExternalStylusButtonsApplied &= ~releasedButtons;
     }
 }
 
@@ -1702,17 +1709,18 @@
     CookedPointerData& currentPointerData = mCurrentCookedState.cookedPointerData;
     const CookedPointerData& lastPointerData = mLastCookedState.cookedPointerData;
 
-    if (mExternalStylusId != -1 && currentPointerData.isTouching(mExternalStylusId)) {
+    if (mFusedStylusPointerId && currentPointerData.isTouching(*mFusedStylusPointerId)) {
         float pressure = mExternalStylusState.pressure;
-        if (pressure == 0.0f && lastPointerData.isTouching(mExternalStylusId)) {
-            const PointerCoords& coords = lastPointerData.pointerCoordsForId(mExternalStylusId);
+        if (pressure == 0.0f && lastPointerData.isTouching(*mFusedStylusPointerId)) {
+            const PointerCoords& coords =
+                    lastPointerData.pointerCoordsForId(*mFusedStylusPointerId);
             pressure = coords.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE);
         }
-        PointerCoords& coords = currentPointerData.editPointerCoordsWithId(mExternalStylusId);
+        PointerCoords& coords = currentPointerData.editPointerCoordsWithId(*mFusedStylusPointerId);
         coords.setAxisValue(AMOTION_EVENT_AXIS_PRESSURE, pressure);
 
         PointerProperties& properties =
-                currentPointerData.editPointerPropertiesWithId(mExternalStylusId);
+                currentPointerData.editPointerPropertiesWithId(*mFusedStylusPointerId);
         if (mExternalStylusState.toolType != AMOTION_EVENT_TOOL_TYPE_UNKNOWN) {
             properties.toolType = mExternalStylusState.toolType;
         }
@@ -1729,10 +1737,11 @@
     if (initialDown) {
         if (mExternalStylusState.pressure != 0.0f) {
             ALOGD_IF(DEBUG_STYLUS_FUSION, "Have both stylus and touch data, beginning fusion");
-            mExternalStylusId = state.rawPointerData.touchingIdBits.firstMarkedBit();
+            mFusedStylusPointerId = state.rawPointerData.touchingIdBits.firstMarkedBit();
         } else if (timeout) {
             ALOGD_IF(DEBUG_STYLUS_FUSION, "Timeout expired, assuming touch is not a stylus.");
-            resetExternalStylus();
+            mFusedStylusPointerId.reset();
+            mExternalStylusFusionTimeout = LLONG_MAX;
         } else {
             if (mExternalStylusFusionTimeout == LLONG_MAX) {
                 mExternalStylusFusionTimeout = state.when + EXTERNAL_STYLUS_DATA_TIMEOUT;
@@ -1746,9 +1755,10 @@
     }
 
     // Check if the stylus pointer has gone up.
-    if (mExternalStylusId != -1 && !state.rawPointerData.touchingIdBits.hasBit(mExternalStylusId)) {
+    if (mFusedStylusPointerId &&
+        !state.rawPointerData.touchingIdBits.hasBit(*mFusedStylusPointerId)) {
         ALOGD_IF(DEBUG_STYLUS_FUSION, "Stylus pointer is going up");
-        mExternalStylusId = -1;
+        mFusedStylusPointerId.reset();
     }
 
     return false;
@@ -1763,7 +1773,7 @@
             out += dispatchPointerGestures(when, readTime, 0 /*policyFlags*/, true /*isTimeout*/);
         }
     } else if (mDeviceMode == DeviceMode::DIRECT) {
-        if (mExternalStylusFusionTimeout < when) {
+        if (mExternalStylusFusionTimeout <= when) {
             out += processRawTouches(true /*timeout*/);
         } else if (mExternalStylusFusionTimeout != LLONG_MAX) {
             getContext()->requestTimeoutAtTime(mExternalStylusFusionTimeout);
@@ -1774,11 +1784,14 @@
 
 std::list<NotifyArgs> TouchInputMapper::updateExternalStylusState(const StylusState& state) {
     std::list<NotifyArgs> out;
+    const bool buttonsChanged = mExternalStylusState.buttons != state.buttons;
     mExternalStylusState.copyFrom(state);
-    if (mExternalStylusId != -1 || mExternalStylusFusionTimeout != LLONG_MAX) {
-        // We're either in the middle of a fused stream of data or we're waiting on data before
-        // dispatching the initial down, so go ahead and dispatch now that we have fresh stylus
-        // data.
+    if (mFusedStylusPointerId || mExternalStylusFusionTimeout != LLONG_MAX || buttonsChanged) {
+        // The following three cases are handled here:
+        // - We're in the middle of a fused stream of data;
+        // - We're waiting on external stylus data before dispatching the initial down; or
+        // - Only the button state, which is not reported through a specific pointer, has changed.
+        // Go ahead and dispatch now that we have fresh stylus data.
         mExternalStylusDataPending = true;
         out += processRawTouches(false /*timeout*/);
     }
diff --git a/services/inputflinger/reader/mapper/TouchInputMapper.h b/services/inputflinger/reader/mapper/TouchInputMapper.h
index c20f28b..fba7b79 100644
--- a/services/inputflinger/reader/mapper/TouchInputMapper.h
+++ b/services/inputflinger/reader/mapper/TouchInputMapper.h
@@ -27,6 +27,13 @@
 
 namespace android {
 
+// Maximum amount of latency to add to touch events while waiting for data from an
+// external stylus.
+static constexpr nsecs_t EXTERNAL_STYLUS_DATA_TIMEOUT = ms2ns(72);
+
+// Maximum amount of time to wait on touch data before pushing out new pressure data.
+static constexpr nsecs_t TOUCH_DATA_TIMEOUT = ms2ns(20);
+
 /* Raw axis information from the driver. */
 struct RawPointerAxes {
     RawAbsoluteAxisInfo x{};
@@ -355,9 +362,14 @@
 
     // State provided by an external stylus
     StylusState mExternalStylusState;
-    int64_t mExternalStylusId;
+    // If an external stylus is capable of reporting pointer-specific data like pressure, we will
+    // attempt to fuse the pointer data reported by the stylus to the first touch pointer. This is
+    // the id of the pointer to which the external stylus data is fused.
+    std::optional<uint32_t> mFusedStylusPointerId;
     nsecs_t mExternalStylusFusionTimeout;
     bool mExternalStylusDataPending;
+    // A subset of the buttons in mCurrentRawState that came from an external stylus.
+    int32_t mExternalStylusButtonsApplied;
 
     // True if we sent a HOVER_ENTER event.
     bool mSentHoverEnter;
diff --git a/services/inputflinger/tests/InputReader_test.cpp b/services/inputflinger/tests/InputReader_test.cpp
index 879d36e..8a361fa 100644
--- a/services/inputflinger/tests/InputReader_test.cpp
+++ b/services/inputflinger/tests/InputReader_test.cpp
@@ -1349,6 +1349,8 @@
         int32_t mGlobalMetaState;
         bool mUpdateGlobalMetaStateWasCalled;
         int32_t mGeneration;
+        std::optional<nsecs_t> mRequestedTimeout;
+        std::vector<InputDeviceInfo> mExternalStylusDevices;
 
     public:
         FakeInputReaderContext(InputReader* reader)
@@ -1382,6 +1384,29 @@
             mGeneration = ContextImpl::bumpGeneration();
             return mGeneration;
         }
+
+        void requestTimeoutAtTime(nsecs_t when) override { mRequestedTimeout = when; }
+
+        void assertTimeoutWasRequested(nsecs_t when) {
+            ASSERT_TRUE(mRequestedTimeout) << "Expected timeout at time " << when
+                                           << " but there was no timeout requested.";
+            ASSERT_EQ(when, *mRequestedTimeout);
+            mRequestedTimeout.reset();
+        }
+
+        void assertTimeoutWasNotRequested() {
+            ASSERT_FALSE(mRequestedTimeout) << "Expected no timeout to have been requested,"
+                                               " but one was requested at time "
+                                            << *mRequestedTimeout;
+        }
+
+        void getExternalStylusDevices(std::vector<InputDeviceInfo>& outDevices) override {
+            outDevices = mExternalStylusDevices;
+        }
+
+        void setExternalStylusDevices(std::vector<InputDeviceInfo>&& devices) {
+            mExternalStylusDevices = devices;
+        }
     } mFakeContext;
 
     friend class InputReaderTest;
@@ -2504,7 +2529,8 @@
     ASSERT_EQ(BTN_GEAR_UP, keyArgs.scanCode);
 }
 
-// --- TouchProcessTest ---
+// --- TouchIntegrationTest ---
+
 class TouchIntegrationTest : public InputReaderIntegrationTest {
 protected:
     const std::string UNIQUE_ID = "local:0";
@@ -2817,108 +2843,294 @@
     ASSERT_NO_FATAL_FAILURE(mFakePolicy->assertStylusGestureNotified(mDeviceInfo.getId()));
 }
 
-TEST_F(TouchIntegrationTest, StylusButtonsGenerateKeyEvents) {
-    mDevice->sendKey(BTN_STYLUS, 1);
-    mDevice->sendSync();
-    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyKeyWasCalled(
-            AllOf(WithKeyAction(AKEY_EVENT_ACTION_DOWN), WithSource(AINPUT_SOURCE_KEYBOARD),
-                  WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY))));
+// --- StylusButtonIntegrationTest ---
 
-    mDevice->sendKey(BTN_STYLUS, 0);
-    mDevice->sendSync();
-    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyKeyWasCalled(
+// Verify the behavior of button presses reported by various kinds of styluses, including buttons
+// reported by the touchscreen's device, by a fused external stylus, and by an un-fused external
+// stylus.
+template <typename UinputStylusDevice>
+class StylusButtonIntegrationTest : public TouchIntegrationTest {
+protected:
+    void SetUp() override {
+#if !defined(__ANDROID__)
+        GTEST_SKIP();
+#endif
+        TouchIntegrationTest::SetUp();
+        mTouchscreen = mDevice.get();
+        mTouchscreenInfo = mDeviceInfo;
+
+        setUpStylusDevice();
+    }
+
+    UinputStylusDevice* mStylus{nullptr};
+    InputDeviceInfo mStylusInfo{};
+
+    UinputTouchScreen* mTouchscreen{nullptr};
+    InputDeviceInfo mTouchscreenInfo{};
+
+private:
+    // When we are attempting to test stylus button events that are sent from the touchscreen,
+    // use the same Uinput device for the touchscreen and the stylus.
+    template <typename T = UinputStylusDevice>
+    std::enable_if_t<std::is_same_v<UinputTouchScreen, T>, void> setUpStylusDevice() {
+        mStylus = mDevice.get();
+        mStylusInfo = mDeviceInfo;
+    }
+
+    // When we are attempting to stylus buttons from an external stylus being merged with touches
+    // from a touchscreen, create a new Uinput device through which stylus buttons can be injected.
+    template <typename T = UinputStylusDevice>
+    std::enable_if_t<!std::is_same_v<UinputTouchScreen, T>, void> setUpStylusDevice() {
+        mStylusDeviceLifecycleTracker = createUinputDevice<T>();
+        mStylus = mStylusDeviceLifecycleTracker.get();
+        ASSERT_NO_FATAL_FAILURE(mFakePolicy->assertInputDevicesChanged());
+        ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyConfigurationChangedWasCalled());
+        const auto info = findDeviceByName(mStylus->getName());
+        ASSERT_TRUE(info);
+        mStylusInfo = *info;
+    }
+
+    std::unique_ptr<UinputStylusDevice> mStylusDeviceLifecycleTracker{};
+
+    // Hide the base class's device to expose it with a different name for readability.
+    using TouchIntegrationTest::mDevice;
+    using TouchIntegrationTest::mDeviceInfo;
+};
+
+using StylusButtonIntegrationTestTypes =
+        ::testing::Types<UinputTouchScreen, UinputExternalStylus, UinputExternalStylusWithPressure>;
+TYPED_TEST_SUITE(StylusButtonIntegrationTest, StylusButtonIntegrationTestTypes);
+
+TYPED_TEST(StylusButtonIntegrationTest, StylusButtonsGenerateKeyEvents) {
+    const auto stylusId = TestFixture::mStylusInfo.getId();
+
+    TestFixture::mStylus->pressKey(BTN_STYLUS);
+    ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyKeyWasCalled(
+            AllOf(WithKeyAction(AKEY_EVENT_ACTION_DOWN), WithSource(AINPUT_SOURCE_KEYBOARD),
+                  WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY), WithDeviceId(stylusId))));
+
+    TestFixture::mStylus->releaseKey(BTN_STYLUS);
+    ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyKeyWasCalled(
             AllOf(WithKeyAction(AKEY_EVENT_ACTION_UP), WithSource(AINPUT_SOURCE_KEYBOARD),
-                  WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY))));
+                  WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY), WithDeviceId(stylusId))));
 }
 
-TEST_F(TouchIntegrationTest, StylusButtonsSurroundingTouchGesture) {
-    const Point centerPoint = mDevice->getCenterPoint();
+TYPED_TEST(StylusButtonIntegrationTest, StylusButtonsSurroundingTouchGesture) {
+    const Point centerPoint = TestFixture::mTouchscreen->getCenterPoint();
+    const auto touchscreenId = TestFixture::mTouchscreenInfo.getId();
+    const auto stylusId = TestFixture::mStylusInfo.getId();
 
     // Press the stylus button.
-    mDevice->sendKey(BTN_STYLUS, 1);
-    mDevice->sendSync();
-    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyKeyWasCalled(
+    TestFixture::mStylus->pressKey(BTN_STYLUS);
+    ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyKeyWasCalled(
             AllOf(WithKeyAction(AKEY_EVENT_ACTION_DOWN), WithSource(AINPUT_SOURCE_KEYBOARD),
-                  WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY))));
+                  WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY), WithDeviceId(stylusId))));
 
     // Start and finish a stylus gesture.
-    mDevice->sendSlot(FIRST_SLOT);
-    mDevice->sendTrackingId(FIRST_TRACKING_ID);
-    mDevice->sendToolType(MT_TOOL_PEN);
-    mDevice->sendDown(centerPoint);
-    mDevice->sendSync();
-    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled(
+    TestFixture::mTouchscreen->sendSlot(FIRST_SLOT);
+    TestFixture::mTouchscreen->sendTrackingId(FIRST_TRACKING_ID);
+    TestFixture::mTouchscreen->sendToolType(MT_TOOL_PEN);
+    TestFixture::mTouchscreen->sendDown(centerPoint);
+    TestFixture::mTouchscreen->sendSync();
+    ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled(
             AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN),
                   WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS),
-                  WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY))));
-    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled(
+                  WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY),
+                  WithDeviceId(touchscreenId))));
+    ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled(
             AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_PRESS),
                   WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS),
-                  WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY))));
+                  WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY),
+                  WithDeviceId(touchscreenId))));
 
-    mDevice->sendTrackingId(INVALID_TRACKING_ID);
-    mDevice->sendSync();
-    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled(
+    TestFixture::mTouchscreen->sendTrackingId(INVALID_TRACKING_ID);
+    TestFixture::mTouchscreen->sendSync();
+    ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled(
             AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_RELEASE),
-                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0))));
-    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled(
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0),
+                  WithDeviceId(touchscreenId))));
+    ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled(
             AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP),
-                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0))));
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0),
+                  WithDeviceId(touchscreenId))));
 
     // Release the stylus button.
-    mDevice->sendKey(BTN_STYLUS, 0);
-    mDevice->sendSync();
-    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyKeyWasCalled(
+    TestFixture::mStylus->releaseKey(BTN_STYLUS);
+    ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyKeyWasCalled(
             AllOf(WithKeyAction(AKEY_EVENT_ACTION_UP), WithSource(AINPUT_SOURCE_KEYBOARD),
-                  WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY))));
+                  WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY), WithDeviceId(stylusId))));
 }
 
-TEST_F(TouchIntegrationTest, StylusButtonsWithinTouchGesture) {
-    const Point centerPoint = mDevice->getCenterPoint();
+TYPED_TEST(StylusButtonIntegrationTest, StylusButtonsWithinTouchGesture) {
+    const Point centerPoint = TestFixture::mTouchscreen->getCenterPoint();
+    const auto touchscreenId = TestFixture::mTouchscreenInfo.getId();
+    const auto stylusId = TestFixture::mStylusInfo.getId();
 
     // Start a stylus gesture.
+    TestFixture::mTouchscreen->sendSlot(FIRST_SLOT);
+    TestFixture::mTouchscreen->sendTrackingId(FIRST_TRACKING_ID);
+    TestFixture::mTouchscreen->sendToolType(MT_TOOL_PEN);
+    TestFixture::mTouchscreen->sendDown(centerPoint);
+    TestFixture::mTouchscreen->sendSync();
+    ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled(
+            AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN),
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0),
+                  WithDeviceId(touchscreenId))));
+
+    // Press and release a stylus button. Each change in button state also generates a MOVE event.
+    TestFixture::mStylus->pressKey(BTN_STYLUS);
+    ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyKeyWasCalled(
+            AllOf(WithKeyAction(AKEY_EVENT_ACTION_DOWN), WithSource(AINPUT_SOURCE_KEYBOARD),
+                  WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY), WithDeviceId(stylusId))));
+    ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled(
+            AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS),
+                  WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY),
+                  WithDeviceId(touchscreenId))));
+    ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled(
+            AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_PRESS),
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS),
+                  WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY),
+                  WithDeviceId(touchscreenId))));
+
+    TestFixture::mStylus->releaseKey(BTN_STYLUS);
+    ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyKeyWasCalled(
+            AllOf(WithKeyAction(AKEY_EVENT_ACTION_UP), WithSource(AINPUT_SOURCE_KEYBOARD),
+                  WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY), WithDeviceId(stylusId))));
+    ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled(
+            AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_RELEASE),
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0),
+                  WithDeviceId(touchscreenId))));
+    ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled(
+            AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0),
+                  WithDeviceId(touchscreenId))));
+
+    // Finish the stylus gesture.
+    TestFixture::mTouchscreen->sendTrackingId(INVALID_TRACKING_ID);
+    TestFixture::mTouchscreen->sendSync();
+    ASSERT_NO_FATAL_FAILURE(TestFixture::mTestListener->assertNotifyMotionWasCalled(
+            AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP),
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0),
+                  WithDeviceId(touchscreenId))));
+}
+
+// --- ExternalStylusIntegrationTest ---
+
+// Verify the behavior of an external stylus. An external stylus can report pressure or button
+// data independently of the touchscreen, which is then sent as a MotionEvent as part of an
+// ongoing stylus gesture that is being emitted by the touchscreen.
+using ExternalStylusIntegrationTest = TouchIntegrationTest;
+
+TEST_F(ExternalStylusIntegrationTest, FusedExternalStylusPressureReported) {
+    const Point centerPoint = mDevice->getCenterPoint();
+
+    // Create an external stylus capable of reporting pressure data that
+    // should be fused with a touch pointer.
+    std::unique_ptr<UinputExternalStylusWithPressure> stylus =
+            createUinputDevice<UinputExternalStylusWithPressure>();
+    ASSERT_NO_FATAL_FAILURE(mFakePolicy->assertInputDevicesChanged());
+    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyConfigurationChangedWasCalled());
+    const auto stylusInfo = findDeviceByName(stylus->getName());
+    ASSERT_TRUE(stylusInfo);
+
+    ASSERT_EQ(AINPUT_SOURCE_STYLUS | AINPUT_SOURCE_KEYBOARD, stylusInfo->getSources());
+
+    const auto touchscreenId = mDeviceInfo.getId();
+
+    // Set a pressure value on the stylus. It doesn't generate any events.
+    const auto& RAW_PRESSURE_MAX = UinputExternalStylusWithPressure::RAW_PRESSURE_MAX;
+    stylus->setPressure(100);
+    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasNotCalled());
+
+    // Start a finger gesture, and ensure it shows up as stylus gesture
+    // with the pressure set by the external stylus.
     mDevice->sendSlot(FIRST_SLOT);
     mDevice->sendTrackingId(FIRST_TRACKING_ID);
-    mDevice->sendToolType(MT_TOOL_PEN);
+    mDevice->sendToolType(MT_TOOL_FINGER);
     mDevice->sendDown(centerPoint);
     mDevice->sendSync();
     ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled(
             AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN),
-                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0))));
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0),
+                  WithDeviceId(touchscreenId), WithPressure(100.f / RAW_PRESSURE_MAX))));
 
-    // Press and release a stylus button. Each change in button state also generates a MOVE event.
-    mDevice->sendKey(BTN_STYLUS, 1);
-    mDevice->sendSync();
-    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyKeyWasCalled(
-            AllOf(WithKeyAction(AKEY_EVENT_ACTION_DOWN), WithSource(AINPUT_SOURCE_KEYBOARD),
-                  WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY))));
+    // Change the pressure on the external stylus, and ensure the touchscreen generates a MOVE
+    // event with the updated pressure.
+    stylus->setPressure(200);
     ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled(
             AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
-                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS),
-                  WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY))));
-    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled(
-            AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_PRESS),
-                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS),
-                  WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY))));
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0),
+                  WithDeviceId(touchscreenId), WithPressure(200.f / RAW_PRESSURE_MAX))));
 
-    mDevice->sendKey(BTN_STYLUS, 0);
+    // The external stylus did not generate any events.
+    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasNotCalled());
+    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyKeyWasNotCalled());
+}
+
+TEST_F(ExternalStylusIntegrationTest, FusedExternalStylusPressureNotReported) {
+    const Point centerPoint = mDevice->getCenterPoint();
+
+    // Create an external stylus capable of reporting pressure data that
+    // should be fused with a touch pointer.
+    std::unique_ptr<UinputExternalStylusWithPressure> stylus =
+            createUinputDevice<UinputExternalStylusWithPressure>();
+    ASSERT_NO_FATAL_FAILURE(mFakePolicy->assertInputDevicesChanged());
+    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyConfigurationChangedWasCalled());
+    const auto stylusInfo = findDeviceByName(stylus->getName());
+    ASSERT_TRUE(stylusInfo);
+
+    ASSERT_EQ(AINPUT_SOURCE_STYLUS | AINPUT_SOURCE_KEYBOARD, stylusInfo->getSources());
+
+    const auto touchscreenId = mDeviceInfo.getId();
+
+    // Set a pressure value of 0 on the stylus. It doesn't generate any events.
+    const auto& RAW_PRESSURE_MAX = UinputExternalStylusWithPressure::RAW_PRESSURE_MAX;
+    stylus->setPressure(0);
+
+    // Start a finger gesture. The touch device will withhold generating any touches for
+    // up to 72 milliseconds while waiting for pressure data from the external stylus.
+    mDevice->sendSlot(FIRST_SLOT);
+    mDevice->sendTrackingId(FIRST_TRACKING_ID);
+    mDevice->sendToolType(MT_TOOL_FINGER);
+    mDevice->sendDown(centerPoint);
+    auto waitUntil = std::chrono::system_clock::now() +
+            std::chrono::milliseconds(ns2ms(EXTERNAL_STYLUS_DATA_TIMEOUT));
     mDevice->sendSync();
-    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyKeyWasCalled(
-            AllOf(WithKeyAction(AKEY_EVENT_ACTION_UP), WithSource(AINPUT_SOURCE_KEYBOARD),
-                  WithKeyCode(AKEYCODE_STYLUS_BUTTON_PRIMARY))));
-    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled(
-            AllOf(WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_RELEASE),
-                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0))));
-    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled(
-            AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
-                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0))));
+    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasNotCalled(waitUntil));
 
-    // Finish the stylus gesture.
+    // Since the external stylus did not report a pressure value within the timeout,
+    // it shows up as a finger pointer.
+    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled(
+            AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN),
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER), WithDeviceId(touchscreenId),
+                  WithPressure(1.f))));
+
+    // Change the pressure on the external stylus. Since the pressure was not present at the start
+    // of the gesture, it is ignored for now.
+    stylus->setPressure(200);
+    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasNotCalled());
+
+    // Finish the finger gesture.
     mDevice->sendTrackingId(INVALID_TRACKING_ID);
     mDevice->sendSync();
     ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled(
             AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP),
-                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0))));
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER))));
+
+    // Start a new gesture. Since we have a valid pressure value, it shows up as a stylus.
+    mDevice->sendTrackingId(FIRST_TRACKING_ID);
+    mDevice->sendToolType(MT_TOOL_FINGER);
+    mDevice->sendDown(centerPoint);
+    mDevice->sendSync();
+    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled(
+            AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN),
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS), WithButtonState(0),
+                  WithDeviceId(touchscreenId), WithPressure(200.f / RAW_PRESSURE_MAX))));
+
+    // The external stylus did not generate any events.
+    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasNotCalled());
+    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyKeyWasNotCalled());
 }
 
 // --- InputDeviceTest ---
@@ -3372,6 +3584,16 @@
         mReader->loopOnce();
     }
 
+    std::list<NotifyArgs> handleTimeout(InputMapper& mapper, nsecs_t when) {
+        std::list<NotifyArgs> generatedArgs = mapper.timeoutExpired(when);
+        for (const NotifyArgs& args : generatedArgs) {
+            mFakeListener->notify(args);
+        }
+        // Loop the reader to flush the input listener queue.
+        mReader->loopOnce();
+        return generatedArgs;
+    }
+
     static void assertMotionRange(const InputDeviceInfo& info,
             int32_t axis, uint32_t source, float min, float max, float flat, float fuzz) {
         const InputDeviceInfo::MotionRange* range = info.getMotionRange(axis, source);
@@ -7566,6 +7788,360 @@
     }
 }
 
+// --- ExternalStylusFusionTest ---
+
+class ExternalStylusFusionTest : public SingleTouchInputMapperTest {
+public:
+    SingleTouchInputMapper& initializeInputMapperWithExternalStylus() {
+        addConfigurationProperty("touch.deviceType", "touchScreen");
+        prepareDisplay(DISPLAY_ORIENTATION_0);
+        prepareButtons();
+        prepareAxes(POSITION);
+        auto& mapper = addMapperAndConfigure<SingleTouchInputMapper>();
+
+        mStylusState.when = ARBITRARY_TIME;
+        mStylusState.pressure = 0.f;
+        mStylusState.toolType = AMOTION_EVENT_TOOL_TYPE_STYLUS;
+        mReader->getContext()->setExternalStylusDevices({mExternalStylusDeviceInfo});
+        configureDevice(InputReaderConfiguration::CHANGE_EXTERNAL_STYLUS_PRESENCE);
+        processExternalStylusState(mapper);
+        return mapper;
+    }
+
+    std::list<NotifyArgs> processExternalStylusState(InputMapper& mapper) {
+        std::list<NotifyArgs> generatedArgs = mapper.updateExternalStylusState(mStylusState);
+        for (const NotifyArgs& args : generatedArgs) {
+            mFakeListener->notify(args);
+        }
+        // Loop the reader to flush the input listener queue.
+        mReader->loopOnce();
+        return generatedArgs;
+    }
+
+protected:
+    StylusState mStylusState{};
+    static constexpr uint32_t EXPECTED_SOURCE =
+            AINPUT_SOURCE_TOUCHSCREEN | AINPUT_SOURCE_BLUETOOTH_STYLUS;
+
+    void testStartFusedStylusGesture(SingleTouchInputMapper& mapper) {
+        auto toolTypeSource =
+                AllOf(WithSource(EXPECTED_SOURCE), WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS));
+
+        // The first pointer is withheld.
+        processDown(mapper, 100, 200);
+        processSync(mapper);
+        ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+        ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasRequested(
+                ARBITRARY_TIME + EXTERNAL_STYLUS_DATA_TIMEOUT));
+
+        // The external stylus reports pressure. The withheld finger pointer is released as a
+        // stylus.
+        mStylusState.pressure = 1.f;
+        processExternalStylusState(mapper);
+        ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+                AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_DOWN))));
+        ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested());
+
+        // Subsequent pointer events are not withheld.
+        processMove(mapper, 101, 201);
+        processSync(mapper);
+        ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+                AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE))));
+
+        ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested());
+        ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+    }
+
+    void testSuccessfulFusionGesture(SingleTouchInputMapper& mapper) {
+        ASSERT_NO_FATAL_FAILURE(testStartFusedStylusGesture(mapper));
+
+        // Releasing the touch pointer ends the gesture.
+        processUp(mapper);
+        processSync(mapper);
+        ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+                AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), WithSource(EXPECTED_SOURCE),
+                      WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS))));
+
+        mStylusState.pressure = 0.f;
+        processExternalStylusState(mapper);
+        ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested());
+        ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+    }
+
+    void testUnsuccessfulFusionGesture(SingleTouchInputMapper& mapper) {
+        auto toolTypeSource =
+                AllOf(WithSource(EXPECTED_SOURCE), WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER));
+
+        // The first pointer is withheld when an external stylus is connected,
+        // and a timeout is requested.
+        processDown(mapper, 100, 200);
+        processSync(mapper);
+        ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+        ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasRequested(
+                ARBITRARY_TIME + EXTERNAL_STYLUS_DATA_TIMEOUT));
+
+        // If the timeout expires early, it is requested again.
+        handleTimeout(mapper, ARBITRARY_TIME + 1);
+        ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasRequested(
+                ARBITRARY_TIME + EXTERNAL_STYLUS_DATA_TIMEOUT));
+
+        // When the timeout expires, the withheld touch is released as a finger pointer.
+        handleTimeout(mapper, ARBITRARY_TIME + EXTERNAL_STYLUS_DATA_TIMEOUT);
+        ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+                AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_DOWN))));
+
+        // Subsequent pointer events are not withheld.
+        processMove(mapper, 101, 201);
+        processSync(mapper);
+        ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+                AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE))));
+        processUp(mapper);
+        processSync(mapper);
+        ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+                AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_UP))));
+
+        ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested());
+        ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+    }
+
+private:
+    InputDeviceInfo mExternalStylusDeviceInfo{};
+};
+
+TEST_F(ExternalStylusFusionTest, UsesBluetoothStylusSource) {
+    SingleTouchInputMapper& mapper = initializeInputMapperWithExternalStylus();
+    ASSERT_EQ(EXPECTED_SOURCE, mapper.getSources());
+}
+
+TEST_F(ExternalStylusFusionTest, UnsuccessfulFusion) {
+    SingleTouchInputMapper& mapper = initializeInputMapperWithExternalStylus();
+    ASSERT_NO_FATAL_FAILURE(testUnsuccessfulFusionGesture(mapper));
+}
+
+TEST_F(ExternalStylusFusionTest, SuccessfulFusion_TouchFirst) {
+    SingleTouchInputMapper& mapper = initializeInputMapperWithExternalStylus();
+    ASSERT_NO_FATAL_FAILURE(testSuccessfulFusionGesture(mapper));
+}
+
+// Test a successful stylus fusion gesture where the pressure is reported by the external
+// before the touch is reported by the touchscreen.
+TEST_F(ExternalStylusFusionTest, SuccessfulFusion_PressureFirst) {
+    SingleTouchInputMapper& mapper = initializeInputMapperWithExternalStylus();
+    auto toolTypeSource =
+            AllOf(WithSource(EXPECTED_SOURCE), WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS));
+
+    // The external stylus reports pressure first. It is ignored for now.
+    mStylusState.pressure = 1.f;
+    processExternalStylusState(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+    ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested());
+
+    // When the touch goes down afterwards, it is reported as a stylus pointer.
+    processDown(mapper, 100, 200);
+    processSync(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_DOWN))));
+    ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested());
+
+    processMove(mapper, 101, 201);
+    processSync(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE))));
+    processUp(mapper);
+    processSync(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_UP))));
+
+    ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested());
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+}
+
+TEST_F(ExternalStylusFusionTest, FusionIsRepeatedForEachNewGesture) {
+    SingleTouchInputMapper& mapper = initializeInputMapperWithExternalStylus();
+
+    ASSERT_NO_FATAL_FAILURE(testSuccessfulFusionGesture(mapper));
+    ASSERT_NO_FATAL_FAILURE(testUnsuccessfulFusionGesture(mapper));
+
+    ASSERT_NO_FATAL_FAILURE(testSuccessfulFusionGesture(mapper));
+    ASSERT_NO_FATAL_FAILURE(testSuccessfulFusionGesture(mapper));
+    ASSERT_NO_FATAL_FAILURE(testUnsuccessfulFusionGesture(mapper));
+    ASSERT_NO_FATAL_FAILURE(testUnsuccessfulFusionGesture(mapper));
+}
+
+TEST_F(ExternalStylusFusionTest, FusedPointerReportsPressureChanges) {
+    SingleTouchInputMapper& mapper = initializeInputMapperWithExternalStylus();
+    auto toolTypeSource =
+            AllOf(WithSource(EXPECTED_SOURCE), WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS));
+
+    mStylusState.pressure = 0.8f;
+    processExternalStylusState(mapper);
+    processDown(mapper, 100, 200);
+    processSync(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_DOWN),
+                  WithPressure(0.8f))));
+    ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested());
+
+    // The external stylus reports a pressure change. We wait for some time for a touch event.
+    mStylusState.pressure = 0.6f;
+    processExternalStylusState(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+    ASSERT_NO_FATAL_FAILURE(
+            mReader->getContext()->assertTimeoutWasRequested(ARBITRARY_TIME + TOUCH_DATA_TIMEOUT));
+
+    // If a touch is reported within the timeout, it reports the updated pressure.
+    processMove(mapper, 101, 201);
+    processSync(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                  WithPressure(0.6f))));
+    ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested());
+
+    // There is another pressure change.
+    mStylusState.pressure = 0.5f;
+    processExternalStylusState(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+    ASSERT_NO_FATAL_FAILURE(
+            mReader->getContext()->assertTimeoutWasRequested(ARBITRARY_TIME + TOUCH_DATA_TIMEOUT));
+
+    // If a touch is not reported within the timeout, a move event is generated to report
+    // the new pressure.
+    handleTimeout(mapper, ARBITRARY_TIME + TOUCH_DATA_TIMEOUT);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                  WithPressure(0.5f))));
+
+    // If a zero pressure is reported before the touch goes up, the previous pressure value is
+    // repeated indefinitely.
+    mStylusState.pressure = 0.0f;
+    processExternalStylusState(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+    ASSERT_NO_FATAL_FAILURE(
+            mReader->getContext()->assertTimeoutWasRequested(ARBITRARY_TIME + TOUCH_DATA_TIMEOUT));
+    processMove(mapper, 102, 202);
+    processSync(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                  WithPressure(0.5f))));
+    processMove(mapper, 103, 203);
+    processSync(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                  WithPressure(0.5f))));
+
+    processUp(mapper);
+    processSync(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), WithSource(EXPECTED_SOURCE),
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS))));
+
+    ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested());
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+}
+
+TEST_F(ExternalStylusFusionTest, FusedPointerReportsToolTypeChanges) {
+    SingleTouchInputMapper& mapper = initializeInputMapperWithExternalStylus();
+    auto source = WithSource(EXPECTED_SOURCE);
+
+    mStylusState.pressure = 1.f;
+    mStylusState.toolType = AMOTION_EVENT_TOOL_TYPE_ERASER;
+    processExternalStylusState(mapper);
+    processDown(mapper, 100, 200);
+    processSync(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(source, WithMotionAction(AMOTION_EVENT_ACTION_DOWN),
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_ERASER))));
+    ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested());
+
+    // The external stylus reports a tool change. We wait for some time for a touch event.
+    mStylusState.toolType = AMOTION_EVENT_TOOL_TYPE_STYLUS;
+    processExternalStylusState(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+    ASSERT_NO_FATAL_FAILURE(
+            mReader->getContext()->assertTimeoutWasRequested(ARBITRARY_TIME + TOUCH_DATA_TIMEOUT));
+
+    // If a touch is reported within the timeout, it reports the updated pressure.
+    processMove(mapper, 101, 201);
+    processSync(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(source, WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS))));
+    ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested());
+
+    // There is another tool type change.
+    mStylusState.toolType = AMOTION_EVENT_TOOL_TYPE_FINGER;
+    processExternalStylusState(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+    ASSERT_NO_FATAL_FAILURE(
+            mReader->getContext()->assertTimeoutWasRequested(ARBITRARY_TIME + TOUCH_DATA_TIMEOUT));
+
+    // If a touch is not reported within the timeout, a move event is generated to report
+    // the new tool type.
+    handleTimeout(mapper, ARBITRARY_TIME + TOUCH_DATA_TIMEOUT);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(source, WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER))));
+
+    processUp(mapper);
+    processSync(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(source, WithMotionAction(AMOTION_EVENT_ACTION_UP),
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_FINGER))));
+
+    ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested());
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+}
+
+TEST_F(ExternalStylusFusionTest, FusedPointerReportsButtons) {
+    SingleTouchInputMapper& mapper = initializeInputMapperWithExternalStylus();
+    auto toolTypeSource =
+            AllOf(WithSource(EXPECTED_SOURCE), WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS));
+
+    ASSERT_NO_FATAL_FAILURE(testStartFusedStylusGesture(mapper));
+
+    // The external stylus reports a button change. We wait for some time for a touch event.
+    mStylusState.buttons = AMOTION_EVENT_BUTTON_STYLUS_PRIMARY;
+    processExternalStylusState(mapper);
+    ASSERT_NO_FATAL_FAILURE(
+            mReader->getContext()->assertTimeoutWasRequested(ARBITRARY_TIME + TOUCH_DATA_TIMEOUT));
+
+    // If a touch is reported within the timeout, it reports the updated button state.
+    processMove(mapper, 101, 201);
+    processSync(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                  WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY))));
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_PRESS),
+                  WithButtonState(AMOTION_EVENT_BUTTON_STYLUS_PRIMARY))));
+    ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested());
+
+    // The button is now released.
+    mStylusState.buttons = 0;
+    processExternalStylusState(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+    ASSERT_NO_FATAL_FAILURE(
+            mReader->getContext()->assertTimeoutWasRequested(ARBITRARY_TIME + TOUCH_DATA_TIMEOUT));
+
+    // If a touch is not reported within the timeout, a move event is generated to report
+    // the new button state.
+    handleTimeout(mapper, ARBITRARY_TIME + TOUCH_DATA_TIMEOUT);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_BUTTON_RELEASE),
+                  WithButtonState(0))));
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_MOVE),
+                  WithButtonState(0))));
+
+    processUp(mapper);
+    processSync(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(toolTypeSource, WithMotionAction(AMOTION_EVENT_ACTION_UP), WithButtonState(0))));
+
+    ASSERT_NO_FATAL_FAILURE(mReader->getContext()->assertTimeoutWasNotRequested());
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasNotCalled());
+}
+
 // --- MultiTouchInputMapperTest ---
 
 class MultiTouchInputMapperTest : public TouchInputMapperTest {
diff --git a/services/inputflinger/tests/TestInputListener.cpp b/services/inputflinger/tests/TestInputListener.cpp
index 5e47b80..a1299ee 100644
--- a/services/inputflinger/tests/TestInputListener.cpp
+++ b/services/inputflinger/tests/TestInputListener.cpp
@@ -82,9 +82,9 @@
     ASSERT_THAT(outEventArgs, matcher);
 }
 
-void TestInputListener::assertNotifyMotionWasNotCalled() {
+void TestInputListener::assertNotifyMotionWasNotCalled(std::optional<TimePoint> waitUntil) {
     ASSERT_NO_FATAL_FAILURE(
-            assertNotCalled<NotifyMotionArgs>("notifyMotion() should not be called."));
+            assertNotCalled<NotifyMotionArgs>("notifyMotion() should not be called.", waitUntil));
 }
 
 void TestInputListener::assertNotifySwitchWasCalled(NotifySwitchArgs* outEventArgs) {
@@ -139,14 +139,16 @@
 }
 
 template <class NotifyArgsType>
-void TestInputListener::assertNotCalled(std::string message) {
+void TestInputListener::assertNotCalled(std::string message, std::optional<TimePoint> waitUntil) {
     std::unique_lock<std::mutex> lock(mLock);
     base::ScopedLockAssertion assumeLocked(mLock);
 
     std::vector<NotifyArgsType>& queue = std::get<std::vector<NotifyArgsType>>(mQueues);
-    const bool eventReceived =
-            mCondition.wait_for(lock, mEventDidNotHappenTimeout,
-                                [&queue]() REQUIRES(mLock) { return !queue.empty(); });
+    const auto time =
+            waitUntil.value_or(std::chrono::system_clock::now() + mEventDidNotHappenTimeout);
+    const bool eventReceived = mCondition.wait_until(lock, time, [&queue]() REQUIRES(mLock) {
+        return !queue.empty();
+    });
     if (eventReceived) {
         FAIL() << "Unexpected event: " << message.c_str();
     }
diff --git a/services/inputflinger/tests/TestInputListener.h b/services/inputflinger/tests/TestInputListener.h
index 87752e1..c53f8e0 100644
--- a/services/inputflinger/tests/TestInputListener.h
+++ b/services/inputflinger/tests/TestInputListener.h
@@ -33,6 +33,8 @@
                       std::chrono::milliseconds eventDidNotHappenTimeout = 0ms);
     virtual ~TestInputListener();
 
+    using TimePoint = std::chrono::time_point<std::chrono::system_clock>;
+
     void assertNotifyConfigurationChangedWasCalled(
             NotifyConfigurationChangedArgs* outEventArgs = nullptr);
 
@@ -52,7 +54,7 @@
 
     void assertNotifyMotionWasCalled(const ::testing::Matcher<NotifyMotionArgs>& matcher);
 
-    void assertNotifyMotionWasNotCalled();
+    void assertNotifyMotionWasNotCalled(std::optional<TimePoint> waitUntil = {});
 
     void assertNotifySwitchWasCalled(NotifySwitchArgs* outEventArgs = nullptr);
 
@@ -66,7 +68,7 @@
     void assertCalled(NotifyArgsType* outEventArgs, std::string message);
 
     template <class NotifyArgsType>
-    void assertNotCalled(std::string message);
+    void assertNotCalled(std::string message, std::optional<TimePoint> timeout = {});
 
     template <class NotifyArgsType>
     void addToQueue(const NotifyArgsType* args);
diff --git a/services/inputflinger/tests/TestInputListenerMatchers.h b/services/inputflinger/tests/TestInputListenerMatchers.h
index 9a47e3e..8721bd8 100644
--- a/services/inputflinger/tests/TestInputListenerMatchers.h
+++ b/services/inputflinger/tests/TestInputListenerMatchers.h
@@ -55,6 +55,11 @@
     return arg.displayId == displayId;
 }
 
+MATCHER_P(WithDeviceId, deviceId, "InputEvent with specified deviceId") {
+    *result_listener << "expected deviceId " << deviceId << ", but got " << arg.deviceId;
+    return arg.deviceId == deviceId;
+}
+
 MATCHER_P(WithKeyCode, keyCode, "KeyEvent with specified key code") {
     *result_listener << "expected key code " << keyCode << ", but got " << arg.keyCode;
     return arg.keyCode == keyCode;
@@ -70,8 +75,8 @@
 
 MATCHER_P(WithPressure, pressure, "InputEvent with specified pressure") {
     const auto argPressure = arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_PRESSURE);
-    *result_listener << "expected pressure " << pressure << ", but got " << pressure;
-    return argPressure;
+    *result_listener << "expected pressure " << pressure << ", but got " << argPressure;
+    return argPressure == pressure;
 }
 
 MATCHER_P(WithToolType, toolType, "InputEvent with specified tool type") {
diff --git a/services/inputflinger/tests/UinputDevice.cpp b/services/inputflinger/tests/UinputDevice.cpp
index bc695b8..97a2614 100644
--- a/services/inputflinger/tests/UinputDevice.cpp
+++ b/services/inputflinger/tests/UinputDevice.cpp
@@ -138,14 +138,36 @@
 UinputExternalStylus::UinputExternalStylus()
       : UinputKeyboard(DEVICE_NAME, PRODUCT_ID, {BTN_STYLUS, BTN_STYLUS2, BTN_STYLUS3}) {}
 
+// --- UinputExternalStylusWithPressure ---
+
+UinputExternalStylusWithPressure::UinputExternalStylusWithPressure()
+      : UinputKeyboard(DEVICE_NAME, PRODUCT_ID, {BTN_STYLUS, BTN_STYLUS2, BTN_STYLUS3}) {}
+
+void UinputExternalStylusWithPressure::configureDevice(int fd, uinput_user_dev* device) {
+    UinputKeyboard::configureDevice(fd, device);
+
+    ioctl(fd, UI_SET_EVBIT, EV_ABS);
+    ioctl(fd, UI_SET_ABSBIT, ABS_PRESSURE);
+    device->absmin[ABS_PRESSURE] = RAW_PRESSURE_MIN;
+    device->absmax[ABS_PRESSURE] = RAW_PRESSURE_MAX;
+}
+
+void UinputExternalStylusWithPressure::setPressure(int32_t pressure) {
+    injectEvent(EV_ABS, ABS_PRESSURE, pressure);
+    injectEvent(EV_SYN, SYN_REPORT, 0);
+}
+
 // --- UinputTouchScreen ---
 
 UinputTouchScreen::UinputTouchScreen(const Rect& size)
-      : UinputDevice(DEVICE_NAME, PRODUCT_ID), mSize(size) {}
+      : UinputKeyboard(DEVICE_NAME, PRODUCT_ID,
+                       {BTN_TOUCH, BTN_TOOL_PEN, BTN_STYLUS, BTN_STYLUS2, BTN_STYLUS3}),
+        mSize(size) {}
 
 void UinputTouchScreen::configureDevice(int fd, uinput_user_dev* device) {
+    UinputKeyboard::configureDevice(fd, device);
+
     // Setup the touch screen device
-    ioctl(fd, UI_SET_EVBIT, EV_KEY);
     ioctl(fd, UI_SET_EVBIT, EV_REL);
     ioctl(fd, UI_SET_EVBIT, EV_ABS);
     ioctl(fd, UI_SET_ABSBIT, ABS_MT_SLOT);
@@ -155,10 +177,6 @@
     ioctl(fd, UI_SET_ABSBIT, ABS_MT_TRACKING_ID);
     ioctl(fd, UI_SET_ABSBIT, ABS_MT_TOOL_TYPE);
     ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT);
-    ioctl(fd, UI_SET_KEYBIT, BTN_TOUCH);
-    ioctl(fd, UI_SET_KEYBIT, BTN_STYLUS);
-    ioctl(fd, UI_SET_KEYBIT, BTN_STYLUS2);
-    ioctl(fd, UI_SET_KEYBIT, BTN_STYLUS3);
 
     device->absmin[ABS_MT_SLOT] = RAW_SLOT_MIN;
     device->absmax[ABS_MT_SLOT] = RAW_SLOT_MAX;
@@ -210,10 +228,6 @@
     injectEvent(EV_SYN, SYN_REPORT, 0);
 }
 
-void UinputTouchScreen::sendKey(int32_t scanCode, int32_t value) {
-    injectEvent(EV_KEY, scanCode, value);
-}
-
 // Get the center x, y base on the range definition.
 const Point UinputTouchScreen::getCenterPoint() {
     return Point(mSize.left + mSize.width() / 2, mSize.top + mSize.height() / 2);
diff --git a/services/inputflinger/tests/UinputDevice.h b/services/inputflinger/tests/UinputDevice.h
index d661bd3..51e331d 100644
--- a/services/inputflinger/tests/UinputDevice.h
+++ b/services/inputflinger/tests/UinputDevice.h
@@ -89,9 +89,9 @@
     explicit UinputKeyboard(const char* name, int16_t productId = PRODUCT_ID,
                             std::initializer_list<int> keys = {});
 
-private:
     void configureDevice(int fd, uinput_user_dev* device) override;
 
+private:
     std::set<int> mKeys;
 };
 
@@ -143,13 +143,35 @@
     explicit UinputExternalStylus();
 };
 
+// --- UinputExternalStylusWithPressure ---
+
+// A stylus that reports button presses and pressure values.
+class UinputExternalStylusWithPressure : public UinputKeyboard {
+public:
+    static constexpr const char* DEVICE_NAME = "Test Uinput External Stylus With Pressure";
+    static constexpr int16_t PRODUCT_ID = 46;
+
+    static constexpr int32_t RAW_PRESSURE_MIN = 0;
+    static constexpr int32_t RAW_PRESSURE_MAX = 255;
+
+    void setPressure(int32_t pressure);
+
+    template <class D, class... Ts>
+    friend std::unique_ptr<D> createUinputDevice(Ts... args);
+
+private:
+    void configureDevice(int fd, uinput_user_dev* device) override;
+
+    explicit UinputExternalStylusWithPressure();
+};
+
 // --- UinputTouchScreen ---
 
 // A multi-touch touchscreen device with specific size that also supports styluses.
-class UinputTouchScreen : public UinputDevice {
+class UinputTouchScreen : public UinputKeyboard {
 public:
     static constexpr const char* DEVICE_NAME = "Test Uinput Touch Screen";
-    static constexpr int16_t PRODUCT_ID = 46;
+    static constexpr int16_t PRODUCT_ID = 47;
 
     static const int32_t RAW_TOUCH_MIN = 0;
     static const int32_t RAW_TOUCH_MAX = 31;
@@ -171,7 +193,6 @@
     void sendUp();
     void sendToolType(int32_t toolType);
     void sendSync();
-    void sendKey(int32_t scanCode, int32_t value);
 
     const Point getCenterPoint();