/*
 * Copyright (C) 2019 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 "DebugConfig.h"
#include "input/InputDevice.h"

#include "InputState.h"

#include <cinttypes>
#include "InputDispatcher.h"

namespace android::inputdispatcher {

InputState::InputState(const IdGenerator& idGenerator) : mIdGenerator(idGenerator) {}

InputState::~InputState() {}

bool InputState::isHovering(DeviceId deviceId, uint32_t source,
                            ui::LogicalDisplayId displayId) const {
    for (const MotionMemento& memento : mMotionMementos) {
        if (memento.deviceId == deviceId && memento.source == source &&
            memento.displayId == displayId && memento.hovering) {
            return true;
        }
    }
    return false;
}

bool InputState::trackKey(const KeyEntry& entry, int32_t flags) {
    switch (entry.action) {
        case AKEY_EVENT_ACTION_UP: {
            if (entry.flags & AKEY_EVENT_FLAG_FALLBACK) {
                std::erase_if(mFallbackKeys,
                              [&entry](const auto& item) { return item.second == entry.keyCode; });
            }
            ssize_t index = findKeyMemento(entry);
            if (index >= 0) {
                mKeyMementos.erase(mKeyMementos.begin() + index);
                return true;
            }
            /* FIXME: We can't just drop the key up event because that prevents creating
             * popup windows that are automatically shown when a key is held and then
             * dismissed when the key is released.  The problem is that the popup will
             * not have received the original key down, so the key up will be considered
             * to be inconsistent with its observed state.  We could perhaps handle this
             * by synthesizing a key down but that will cause other problems.
             *
             * So for now, allow inconsistent key up events to be dispatched.
             *
    #if DEBUG_OUTBOUND_EVENT_DETAILS
            ALOGD("Dropping inconsistent key up event: deviceId=%d, source=%08x, "
                    "keyCode=%d, scanCode=%d",
                    entry.deviceId, entry.source, entry.keyCode, entry.scanCode);
    #endif
            return false;
            */
            return true;
        }

        case AKEY_EVENT_ACTION_DOWN: {
            ssize_t index = findKeyMemento(entry);
            if (index >= 0) {
                mKeyMementos.erase(mKeyMementos.begin() + index);
            }
            addKeyMemento(entry, flags);
            return true;
        }

        default:
            return true;
    }
}

/**
 * Return:
 *  true if the incoming event was correctly tracked,
 *  false if the incoming event should be dropped.
 */
bool InputState::trackMotion(const MotionEntry& entry, int32_t flags) {
    // Don't track non-pointer events
    if (!isFromSource(entry.source, AINPUT_SOURCE_CLASS_POINTER)) {
        // This is a focus-dispatched event; we don't track its state.
        return true;
    }

    if (!input_flags::enable_multi_device_same_window_stream()) {
        if (!mMotionMementos.empty()) {
            const MotionMemento& lastMemento = mMotionMementos.back();
            if (isStylusEvent(lastMemento.source, lastMemento.pointerProperties) &&
                !isStylusEvent(entry.source, entry.pointerProperties)) {
                // We already have a stylus stream, and the new event is not from stylus.
                return false;
            }
        }
    }

    int32_t actionMasked = entry.action & AMOTION_EVENT_ACTION_MASK;
    switch (actionMasked) {
        case AMOTION_EVENT_ACTION_UP:
        case AMOTION_EVENT_ACTION_CANCEL: {
            ssize_t index = findMotionMemento(entry, /*hovering=*/false);
            if (index >= 0) {
                mMotionMementos.erase(mMotionMementos.begin() + index);
                return true;
            }

            return false;
        }

        case AMOTION_EVENT_ACTION_DOWN: {
            ssize_t index = findMotionMemento(entry, /*hovering=*/false);
            if (index >= 0) {
                mMotionMementos.erase(mMotionMementos.begin() + index);
            }
            addMotionMemento(entry, flags, /*hovering=*/false);
            return true;
        }

        case AMOTION_EVENT_ACTION_POINTER_UP:
        case AMOTION_EVENT_ACTION_POINTER_DOWN:
        case AMOTION_EVENT_ACTION_MOVE: {
            if (entry.source & AINPUT_SOURCE_CLASS_NAVIGATION) {
                // Trackballs can send MOVE events with a corresponding DOWN or UP. There's no need
                // to generate cancellation events for these since they're based in relative rather
                // than absolute units.
                return true;
            }

            ssize_t index = findMotionMemento(entry, /*hovering=*/false);

            if (entry.source & AINPUT_SOURCE_CLASS_JOYSTICK) {
                // Joysticks can send MOVE events without a corresponding DOWN or UP. Since all
                // joystick axes are normalized to [-1, 1] we can trust that 0 means it's neutral.
                // Any other value and we need to track the motion so we can send cancellation
                // events for anything generating fallback events (e.g. DPad keys for joystick
                // movements).
                if (index >= 0) {
                    if (entry.pointerCoords[0].isEmpty()) {
                        mMotionMementos.erase(mMotionMementos.begin() + index);
                    } else {
                        MotionMemento& memento = mMotionMementos[index];
                        memento.setPointers(entry);
                    }
                } else if (!entry.pointerCoords[0].isEmpty()) {
                    addMotionMemento(entry, flags, /*hovering=*/false);
                }

                // Joysticks and trackballs can send MOVE events without corresponding DOWN or UP.
                return true;
            }

            if (index >= 0) {
                MotionMemento& memento = mMotionMementos[index];
                if (memento.firstNewPointerIdx < 0) {
                    memento.setPointers(entry);
                    return true;
                }
            }

            return false;
        }

        case AMOTION_EVENT_ACTION_HOVER_EXIT: {
            ssize_t index = findMotionMemento(entry, /*hovering=*/true);
            if (index >= 0) {
                mMotionMementos.erase(mMotionMementos.begin() + index);
                return true;
            }

            return false;
        }

        case AMOTION_EVENT_ACTION_HOVER_ENTER:
        case AMOTION_EVENT_ACTION_HOVER_MOVE: {
            ssize_t index = findMotionMemento(entry, /*hovering=*/true);
            if (index >= 0) {
                mMotionMementos.erase(mMotionMementos.begin() + index);
            }
            addMotionMemento(entry, flags, /*hovering=*/true);
            return true;
        }

        default:
            return true;
    }
}

std::optional<std::pair<std::vector<PointerProperties>, std::vector<PointerCoords>>>
InputState::getPointersOfLastEvent(const MotionEntry& entry, bool hovering) const {
    ssize_t index = findMotionMemento(entry, hovering);
    if (index == -1) {
        return std::nullopt;
    }
    return std::make_pair(mMotionMementos[index].pointerProperties,
                          mMotionMementos[index].pointerCoords);
}

ssize_t InputState::findKeyMemento(const KeyEntry& entry) const {
    for (size_t i = 0; i < mKeyMementos.size(); i++) {
        const KeyMemento& memento = mKeyMementos[i];
        if (memento.deviceId == entry.deviceId && memento.source == entry.source &&
            memento.displayId == entry.displayId && memento.keyCode == entry.keyCode &&
            memento.scanCode == entry.scanCode) {
            return i;
        }
    }
    return -1;
}

ssize_t InputState::findMotionMemento(const MotionEntry& entry, bool hovering) const {
    for (size_t i = 0; i < mMotionMementos.size(); i++) {
        const MotionMemento& memento = mMotionMementos[i];
        if (memento.deviceId == entry.deviceId && memento.source == entry.source &&
            memento.displayId == entry.displayId && memento.hovering == hovering) {
            return i;
        }
    }
    return -1;
}

void InputState::addKeyMemento(const KeyEntry& entry, int32_t flags) {
    KeyMemento memento;
    memento.deviceId = entry.deviceId;
    memento.source = entry.source;
    memento.displayId = entry.displayId;
    memento.keyCode = entry.keyCode;
    memento.scanCode = entry.scanCode;
    memento.metaState = entry.metaState;
    memento.flags = flags;
    memento.downTime = entry.downTime;
    memento.policyFlags = entry.policyFlags;
    mKeyMementos.push_back(memento);
}

void InputState::addMotionMemento(const MotionEntry& entry, int32_t flags, bool hovering) {
    MotionMemento memento;
    memento.deviceId = entry.deviceId;
    memento.source = entry.source;
    memento.displayId = entry.displayId;
    memento.flags = flags;
    memento.xPrecision = entry.xPrecision;
    memento.yPrecision = entry.yPrecision;
    memento.xCursorPosition = entry.xCursorPosition;
    memento.yCursorPosition = entry.yCursorPosition;
    memento.downTime = entry.downTime;
    memento.setPointers(entry);
    memento.hovering = hovering;
    memento.policyFlags = entry.policyFlags;
    mMotionMementos.push_back(memento);
}

void InputState::MotionMemento::setPointers(const MotionEntry& entry) {
    pointerProperties.clear();
    pointerCoords.clear();

    for (uint32_t i = 0; i < entry.getPointerCount(); i++) {
        if (MotionEvent::getActionMasked(entry.action) == AMOTION_EVENT_ACTION_POINTER_UP) {
            // In POINTER_UP events, the pointer is leaving. Since the action is not stored,
            // this departing pointer should not be recorded.
            const uint8_t actionIndex = MotionEvent::getActionIndex(entry.action);
            if (i == actionIndex) {
                continue;
            }
        }
        pointerProperties.push_back(entry.pointerProperties[i]);
        pointerCoords.push_back(entry.pointerCoords[i]);
    }
}

void InputState::MotionMemento::mergePointerStateTo(MotionMemento& other) const {
    for (uint32_t i = 0; i < getPointerCount(); i++) {
        if (other.firstNewPointerIdx < 0) {
            other.firstNewPointerIdx = other.getPointerCount();
        }
        other.pointerProperties.push_back(pointerProperties[i]);
        other.pointerCoords.push_back(pointerCoords[i]);
    }
}

size_t InputState::MotionMemento::getPointerCount() const {
    return pointerProperties.size();
}

bool InputState::shouldCancelPreviousStream(const MotionEntry& motionEntry) const {
    if (!isFromSource(motionEntry.source, AINPUT_SOURCE_CLASS_POINTER)) {
        // This is a focus-dispatched event that should not affect the previous stream.
        return false;
    }

    // New MotionEntry pointer event is coming in.

    // If this is a new gesture, and it's from a different device, then, in general, we will cancel
    // the current gesture.
    // However, because stylus should be preferred over touch, we need to treat some cases in a
    // special way.
    if (mMotionMementos.empty()) {
        // There is no ongoing pointer gesture, so there is nothing to cancel
        return false;
    }

    const MotionMemento& lastMemento = mMotionMementos.back();
    const int32_t actionMasked = MotionEvent::getActionMasked(motionEntry.action);

    // For compatibility, only one input device can be active at a time in the same window.
    if (lastMemento.deviceId == motionEntry.deviceId) {
        // In general, the same device should produce self-consistent streams so nothing needs to
        // be canceled. But there is one exception:
        // Sometimes ACTION_DOWN is received without a corresponding HOVER_EXIT. To account for
        // that, cancel the previous hovering stream
        if (actionMasked == AMOTION_EVENT_ACTION_DOWN && lastMemento.hovering) {
            return true;
        }

        // If the stream changes its source, just cancel the current gesture to be safe. It's
        // possible that the app isn't handling source changes properly
        if (motionEntry.source != lastMemento.source) {
            LOG(INFO) << "Canceling stream: last source was "
                      << inputEventSourceToString(lastMemento.source) << " and new event is "
                      << motionEntry;
            return true;
        }

        // If the injection is happening into two different displays, the same injected device id
        // could be going into both. And at this time, if mirroring is active, the same connection
        // would receive different events from each display. Since the TouchStates are per-display,
        // it's unlikely that those two streams would be consistent with each other. Therefore,
        // cancel the previous gesture if the display id changes.
        if (motionEntry.displayId != lastMemento.displayId) {
            LOG(INFO) << "Canceling stream: last displayId was " << lastMemento.displayId
                      << " and new event is " << motionEntry;
            return true;
        }

        return false;
    }

    if (!input_flags::enable_multi_device_same_window_stream()) {
        if (isStylusEvent(lastMemento.source, lastMemento.pointerProperties)) {
            // A stylus is already active.
            if (isStylusEvent(motionEntry.source, motionEntry.pointerProperties) &&
                actionMasked == AMOTION_EVENT_ACTION_DOWN) {
                // If this new event is from a different device, then cancel the old
                // stylus and allow the new stylus to take over, but only if it's going down.
                // Otherwise, they will start to race each other.
                return true;
            }

            // Keep the current stylus gesture.
            return false;
        }

        // Cancel the current gesture if this is a start of a new gesture from a new device.
        if (actionMasked == AMOTION_EVENT_ACTION_DOWN ||
            actionMasked == AMOTION_EVENT_ACTION_HOVER_ENTER) {
            return true;
        }
    }
    // By default, don't cancel any events.
    return false;
}

std::unique_ptr<EventEntry> InputState::cancelConflictingInputStream(
        const MotionEntry& motionEntry) {
    if (!shouldCancelPreviousStream(motionEntry)) {
        return {};
    }

    const MotionMemento& memento = mMotionMementos.back();

    // Cancel the last device stream
    std::unique_ptr<MotionEntry> cancelEntry =
            createCancelEntryForMemento(memento, motionEntry.eventTime);

    if (!trackMotion(*cancelEntry, cancelEntry->flags)) {
        LOG(FATAL) << "Generated inconsistent cancel event!";
    }
    return cancelEntry;
}

std::unique_ptr<MotionEntry> InputState::createCancelEntryForMemento(const MotionMemento& memento,
                                                                     nsecs_t eventTime) const {
    const int32_t action =
            memento.hovering ? AMOTION_EVENT_ACTION_HOVER_EXIT : AMOTION_EVENT_ACTION_CANCEL;
    int32_t flags = memento.flags;
    if (action == AMOTION_EVENT_ACTION_CANCEL) {
        flags |= AMOTION_EVENT_FLAG_CANCELED;
    }
    return std::make_unique<MotionEntry>(mIdGenerator.nextId(), /*injectionState=*/nullptr,
                                         eventTime, memento.deviceId, memento.source,
                                         memento.displayId, memento.policyFlags, action,
                                         /*actionButton=*/0, flags, AMETA_NONE,
                                         /*buttonState=*/0, MotionClassification::NONE,
                                         AMOTION_EVENT_EDGE_FLAG_NONE, memento.xPrecision,
                                         memento.yPrecision, memento.xCursorPosition,
                                         memento.yCursorPosition, memento.downTime,
                                         memento.pointerProperties, memento.pointerCoords);
}

std::vector<std::unique_ptr<EventEntry>> InputState::synthesizeCancelationEvents(
        nsecs_t currentTime, const CancelationOptions& options) {
    std::vector<std::unique_ptr<EventEntry>> events;
    for (KeyMemento& memento : mKeyMementos) {
        if (shouldCancelKey(memento, options)) {
            events.push_back(
                    std::make_unique<KeyEntry>(mIdGenerator.nextId(), /*injectionState=*/nullptr,
                                               currentTime, memento.deviceId, memento.source,
                                               memento.displayId, memento.policyFlags,
                                               AKEY_EVENT_ACTION_UP,
                                               memento.flags | AKEY_EVENT_FLAG_CANCELED,
                                               memento.keyCode, memento.scanCode, memento.metaState,
                                               /*repeatCount=*/0, memento.downTime));
        }
    }

    for (const MotionMemento& memento : mMotionMementos) {
        if (shouldCancelMotion(memento, options)) {
            if (options.pointerIds == std::nullopt) {
                events.push_back(createCancelEntryForMemento(memento, currentTime));
            } else {
                std::vector<std::unique_ptr<MotionEntry>> pointerCancelEvents =
                        synthesizeCancelationEventsForPointers(memento, options.pointerIds.value(),
                                                               currentTime);
                events.insert(events.end(), std::make_move_iterator(pointerCancelEvents.begin()),
                              std::make_move_iterator(pointerCancelEvents.end()));
            }
        }
    }
    return events;
}

std::vector<std::unique_ptr<EventEntry>> InputState::synthesizePointerDownEvents(
        nsecs_t currentTime) {
    std::vector<std::unique_ptr<EventEntry>> events;
    for (MotionMemento& memento : mMotionMementos) {
        if (!isFromSource(memento.source, AINPUT_SOURCE_CLASS_POINTER)) {
            continue;
        }

        if (memento.firstNewPointerIdx < 0) {
            continue;
        }

        std::vector<PointerProperties> pointerProperties;
        std::vector<PointerCoords> pointerCoords;

        // We will deliver all pointers the target already knows about
        for (uint32_t i = 0; i < static_cast<uint32_t>(memento.firstNewPointerIdx); i++) {
            pointerProperties.push_back(memento.pointerProperties[i]);
            pointerCoords.push_back(memento.pointerCoords[i]);
        }

        // We will send explicit events for all pointers the target doesn't know about
        for (uint32_t i = static_cast<uint32_t>(memento.firstNewPointerIdx);
             i < memento.getPointerCount(); i++) {
            pointerProperties.push_back(memento.pointerProperties[i]);
            pointerCoords.push_back(memento.pointerCoords[i]);

            const size_t pointerCount = pointerProperties.size();

            // Down only if the first pointer, pointer down otherwise
            const int32_t action = (pointerCount <= 1)
                    ? AMOTION_EVENT_ACTION_DOWN
                    : AMOTION_EVENT_ACTION_POINTER_DOWN
                            | (i << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);

            events.push_back(
                    std::make_unique<MotionEntry>(mIdGenerator.nextId(), /*injectionState=*/nullptr,
                                                  currentTime, memento.deviceId, memento.source,
                                                  memento.displayId, memento.policyFlags, action,
                                                  /*actionButton=*/0, memento.flags, AMETA_NONE,
                                                  /*buttonState=*/0, MotionClassification::NONE,
                                                  AMOTION_EVENT_EDGE_FLAG_NONE, memento.xPrecision,
                                                  memento.yPrecision, memento.xCursorPosition,
                                                  memento.yCursorPosition, memento.downTime,
                                                  pointerProperties, pointerCoords));
        }

        memento.firstNewPointerIdx = INVALID_POINTER_INDEX;
    }

    return events;
}

std::vector<std::unique_ptr<MotionEntry>> InputState::synthesizeCancelationEventsForPointers(
        const MotionMemento& memento, std::bitset<MAX_POINTER_ID + 1> pointerIds,
        nsecs_t currentTime) {
    std::vector<std::unique_ptr<MotionEntry>> events;
    std::vector<uint32_t> canceledPointerIndices;

    for (uint32_t pointerIdx = 0; pointerIdx < memento.getPointerCount(); pointerIdx++) {
        uint32_t pointerId = uint32_t(memento.pointerProperties[pointerIdx].id);
        if (pointerIds.test(pointerId)) {
            canceledPointerIndices.push_back(pointerIdx);
        }
    }

    if (canceledPointerIndices.size() == memento.getPointerCount()) {
        // We are cancelling all pointers.
        events.emplace_back(createCancelEntryForMemento(memento, currentTime));
        return events;
    }

    // If we aren't canceling all pointers, we need to generate ACTION_POINTER_UP with
    // FLAG_CANCELED for each of the canceled pointers. For each event, we must remove the
    // previously canceled pointers from PointerProperties and PointerCoords, and update
    // pointerCount appropriately. For convenience, sort the canceled pointer indices in
    // descending order so that we can just slide the remaining pointers to the beginning of
    // the array when a pointer is canceled.
    std::sort(canceledPointerIndices.begin(), canceledPointerIndices.end(),
              std::greater<uint32_t>());

    std::vector<PointerProperties> pointerProperties = memento.pointerProperties;
    std::vector<PointerCoords> pointerCoords = memento.pointerCoords;
    for (const uint32_t pointerIdx : canceledPointerIndices) {
        if (pointerProperties.size() <= 1) {
            LOG(FATAL) << "Unexpected code path for canceling all pointers!";
        }
        const int32_t action = AMOTION_EVENT_ACTION_POINTER_UP |
                (pointerIdx << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);
        events.push_back(
                std::make_unique<MotionEntry>(mIdGenerator.nextId(), /*injectionState=*/nullptr,
                                              currentTime, memento.deviceId, memento.source,
                                              memento.displayId, memento.policyFlags, action,
                                              /*actionButton=*/0,
                                              memento.flags | AMOTION_EVENT_FLAG_CANCELED,
                                              AMETA_NONE, /*buttonState=*/0,
                                              MotionClassification::NONE,
                                              AMOTION_EVENT_EDGE_FLAG_NONE, memento.xPrecision,
                                              memento.yPrecision, memento.xCursorPosition,
                                              memento.yCursorPosition, memento.downTime,
                                              pointerProperties, pointerCoords));

        // Cleanup pointer information
        pointerProperties.erase(pointerProperties.begin() + pointerIdx);
        pointerCoords.erase(pointerCoords.begin() + pointerIdx);
    }
    return events;
}

void InputState::clear() {
    mKeyMementos.clear();
    mMotionMementos.clear();
    mFallbackKeys.clear();
}

void InputState::mergePointerStateTo(InputState& other) {
    for (size_t i = 0; i < mMotionMementos.size(); i++) {
        MotionMemento& memento = mMotionMementos[i];
        // Since we support split pointers we need to merge touch events
        // from the same source + device + screen.
        if (isFromSource(memento.source, AINPUT_SOURCE_CLASS_POINTER)) {
            bool merged = false;
            for (size_t j = 0; j < other.mMotionMementos.size(); j++) {
                MotionMemento& otherMemento = other.mMotionMementos[j];
                if (memento.deviceId == otherMemento.deviceId &&
                    memento.source == otherMemento.source &&
                    memento.displayId == otherMemento.displayId) {
                    memento.mergePointerStateTo(otherMemento);
                    merged = true;
                    break;
                }
            }
            if (!merged) {
                memento.firstNewPointerIdx = 0;
                other.mMotionMementos.push_back(memento);
            }
        }
    }
}

std::optional<int32_t> InputState::getFallbackKey(int32_t originalKeyCode) {
    auto it = mFallbackKeys.find(originalKeyCode);
    if (it == mFallbackKeys.end()) {
        return {};
    }
    return it->second;
}

void InputState::setFallbackKey(int32_t originalKeyCode, int32_t fallbackKeyCode) {
    mFallbackKeys.insert_or_assign(originalKeyCode, fallbackKeyCode);
}

void InputState::removeFallbackKey(int32_t originalKeyCode) {
    mFallbackKeys.erase(originalKeyCode);
}

bool InputState::shouldCancelKey(const KeyMemento& memento, const CancelationOptions& options) {
    if (options.keyCode && memento.keyCode != options.keyCode.value()) {
        return false;
    }

    if (options.deviceId && memento.deviceId != options.deviceId.value()) {
        return false;
    }

    if (options.displayId && memento.displayId != options.displayId.value()) {
        return false;
    }

    switch (options.mode) {
        case CancelationOptions::Mode::CANCEL_ALL_EVENTS:
        case CancelationOptions::Mode::CANCEL_NON_POINTER_EVENTS:
            return true;
        case CancelationOptions::Mode::CANCEL_FALLBACK_EVENTS:
            return memento.flags & AKEY_EVENT_FLAG_FALLBACK;
        default:
            return false;
    }
}

bool InputState::shouldCancelMotion(const MotionMemento& memento,
                                    const CancelationOptions& options) {
    if (options.deviceId && memento.deviceId != options.deviceId.value()) {
        return false;
    }

    if (options.displayId && memento.displayId != options.displayId.value()) {
        return false;
    }

    switch (options.mode) {
        case CancelationOptions::Mode::CANCEL_ALL_EVENTS:
            return true;
        case CancelationOptions::Mode::CANCEL_POINTER_EVENTS:
            return memento.source & AINPUT_SOURCE_CLASS_POINTER;
        case CancelationOptions::Mode::CANCEL_NON_POINTER_EVENTS:
            return !(memento.source & AINPUT_SOURCE_CLASS_POINTER);
        case CancelationOptions::Mode::CANCEL_HOVER_EVENTS:
            return memento.hovering;
        default:
            return false;
    }
}

std::ostream& operator<<(std::ostream& out, const InputState& state) {
    if (!state.mMotionMementos.empty()) {
        out << "mMotionMementos: ";
        for (const InputState::MotionMemento& memento : state.mMotionMementos) {
            out << "{deviceId= " << memento.deviceId << ", hovering=" << memento.hovering << "}, ";
        }
    }
    return out;
}

} // namespace android::inputdispatcher
