Convert InputVerifier to rust

To establish some basic rust infrastructure for input code, convert the
InputVerifier into rust.

Currently, we use bindgen for interfacing between cpp and rust. In a
future CL, this may be changed to an aidl interface instead.

The logs and verifications can be enabled via:
adb shell setprop log.tag.InputTransportVerifyEvents DEBUG
adb shell setprop log.tag.InputVerifierLogEvents DEBUG
adb shell setprop log.tag.InputDispatcherVerifyEvents DEBUG

Bug: 271455682
Test: m inputflinger_tests && $ANDROID_HOST_OUT/nativetest64/inputflinger_tests/inputflinger_tests
Change-Id: I607fed9f6fc9c38e2c8392f59e9c4facdaf6c68a
diff --git a/libs/input/Android.bp b/libs/input/Android.bp
index 4be7328..80a8c8d 100644
--- a/libs/input/Android.bp
+++ b/libs/input/Android.bp
@@ -33,6 +33,138 @@
     ],
 }
 
+aidl_interface {
+    name: "inputconstants",
+    host_supported: true,
+    vendor_available: true,
+    unstable: true,
+    srcs: [
+        ":inputconstants_aidl",
+    ],
+
+    backend: {
+        rust: {
+            enabled: true,
+        },
+    },
+}
+
+rust_bindgen {
+    name: "libinput_bindgen",
+    host_supported: true,
+    crate_name: "input_bindgen",
+    visibility: ["//frameworks/native/services/inputflinger"],
+    wrapper_src: "InputWrapper.hpp",
+
+    include_dirs: [
+        "frameworks/native/include",
+    ],
+
+    source_stem: "bindings",
+
+    bindgen_flags: [
+        "--verbose",
+        "--allowlist-var=AMOTION_EVENT_FLAG_CANCELED",
+        "--allowlist-var=AMOTION_EVENT_ACTION_CANCEL",
+        "--allowlist-var=AMOTION_EVENT_ACTION_UP",
+        "--allowlist-var=AMOTION_EVENT_ACTION_POINTER_DOWN",
+        "--allowlist-var=AMOTION_EVENT_ACTION_DOWN",
+        "--allowlist-var=AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT",
+        "--allowlist-var=MAX_POINTER_ID",
+    ],
+
+    static_libs: [
+        "inputconstants-cpp",
+        "libui-types",
+    ],
+    shared_libs: ["libc++"],
+    header_libs: [
+        "native_headers",
+        "jni_headers",
+        "flatbuffer_headers",
+    ],
+}
+
+// Contains methods to help access C++ code from rust
+cc_library_static {
+    name: "libinput_from_rust_to_cpp",
+    cpp_std: "c++20",
+    host_supported: true,
+    cflags: [
+        "-Wall",
+        "-Wextra",
+        "-Werror",
+    ],
+    srcs: [
+        "FromRustToCpp.cpp",
+    ],
+
+    generated_headers: [
+        "cxx-bridge-header",
+    ],
+    generated_sources: ["libinput_cxx_bridge_code"],
+
+    shared_libs: [
+        "libbase",
+    ],
+}
+
+genrule {
+    name: "libinput_cxx_bridge_code",
+    tools: ["cxxbridge"],
+    cmd: "$(location cxxbridge) $(in) >> $(out)",
+    srcs: ["input_verifier.rs"],
+    out: ["inputverifier_generated.cpp"],
+}
+
+genrule {
+    name: "libinput_cxx_bridge_header",
+    tools: ["cxxbridge"],
+    cmd: "$(location cxxbridge) $(in) --header >> $(out)",
+    srcs: ["input_verifier.rs"],
+    out: ["input_verifier.rs.h"],
+}
+
+rust_defaults {
+    name: "libinput_rust_defaults",
+    srcs: ["input_verifier.rs"],
+    host_supported: true,
+    rustlibs: [
+        "libbitflags",
+        "libcxx",
+        "libinput_bindgen",
+        "liblogger",
+        "liblog_rust",
+        "inputconstants-rust",
+    ],
+
+    shared_libs: [
+        "libbase",
+        "liblog",
+    ],
+}
+
+rust_ffi_static {
+    name: "libinput_rust",
+    crate_name: "input",
+    defaults: ["libinput_rust_defaults"],
+}
+
+rust_test {
+    name: "libinput_rust_test",
+    defaults: ["libinput_rust_defaults"],
+    whole_static_libs: [
+        "libinput_from_rust_to_cpp",
+    ],
+    test_options: {
+        unit_test: true,
+    },
+    test_suites: ["device_tests"],
+    sanitize: {
+        hwaddress: true,
+    },
+}
+
 cc_library {
     name: "libinput",
     cpp_std: "c++20",
@@ -44,6 +176,7 @@
         "-Wno-unused-parameter",
     ],
     srcs: [
+        "FromRustToCpp.cpp",
         "Input.cpp",
         "InputDevice.cpp",
         "InputEventLabels.cpp",
@@ -70,9 +203,13 @@
     export_header_lib_headers: ["jni_headers"],
 
     generated_headers: [
+        "cxx-bridge-header",
+        "libinput_cxx_bridge_header",
         "toolbox_input_labels",
     ],
 
+    generated_sources: ["libinput_cxx_bridge_code"],
+
     shared_libs: [
         "libbase",
         "libcutils",
@@ -92,20 +229,29 @@
     },
 
     static_libs: [
+        "inputconstants-cpp",
         "libui-types",
         "libtflite_static",
     ],
 
+    whole_static_libs: [
+        "libinput_rust",
+    ],
+
     export_static_lib_headers: [
         "libui-types",
     ],
 
+    export_generated_headers: [
+        "cxx-bridge-header",
+        "libinput_cxx_bridge_header",
+    ],
+
     target: {
         android: {
             srcs: [
                 "InputTransport.cpp",
                 "android/os/IInputFlinger.aidl",
-                ":inputconstants_aidl",
             ],
 
             export_shared_lib_headers: ["libbinder"],
@@ -140,9 +286,6 @@
         host_linux: {
             srcs: [
                 "InputTransport.cpp",
-                "android/os/IInputConstants.aidl",
-                "android/os/IInputFlinger.aidl",
-                "android/os/InputConfig.aidl",
             ],
             static_libs: [
                 "libhostgraphics",
diff --git a/libs/input/FromRustToCpp.cpp b/libs/input/FromRustToCpp.cpp
new file mode 100644
index 0000000..e4ce62e
--- /dev/null
+++ b/libs/input/FromRustToCpp.cpp
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 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 <android-base/logging.h>
+#include <ffi/FromRustToCpp.h>
+
+namespace android {
+
+bool shouldLog(rust::Str tag) {
+    return android::base::ShouldLog(android::base::LogSeverity::DEBUG, tag.data());
+}
+
+} // namespace android
diff --git a/libs/input/InputTransport.cpp b/libs/input/InputTransport.cpp
index 4ecb641..4d3d8bc 100644
--- a/libs/input/InputTransport.cpp
+++ b/libs/input/InputTransport.cpp
@@ -14,6 +14,7 @@
 #include <sys/types.h>
 #include <unistd.h>
 
+#include <android-base/logging.h>
 #include <android-base/properties.h>
 #include <android-base/stringprintf.h>
 #include <binder/Parcel.h>
@@ -81,6 +82,7 @@
 
 } // namespace
 
+using android::base::Result;
 using android::base::StringPrintf;
 
 namespace android {
@@ -621,8 +623,12 @@
         ATRACE_NAME(message.c_str());
     }
     if (verifyEvents()) {
-        mInputVerifier.processMovement(deviceId, action, pointerCount, pointerProperties,
-                                       pointerCoords, flags);
+        Result<void> result =
+                mInputVerifier.processMovement(deviceId, action, pointerCount, pointerProperties,
+                                               pointerCoords, flags);
+        if (!result.ok()) {
+            LOG(FATAL) << "Bad stream: " << result.error();
+        }
     }
     if (debugTransportPublisher()) {
         std::string transformString;
diff --git a/libs/input/InputVerifier.cpp b/libs/input/InputVerifier.cpp
index eb75804..32b4ca0 100644
--- a/libs/input/InputVerifier.cpp
+++ b/libs/input/InputVerifier.cpp
@@ -18,111 +18,35 @@
 
 #include <android-base/logging.h>
 #include <input/InputVerifier.h>
+#include "input_verifier.rs.h"
+
+using android::base::Error;
+using android::base::Result;
+using android::input::RustPointerProperties;
 
 namespace android {
 
-/**
- * Log all of the movements that are sent to this verifier. Helps to identify the streams that lead
- * to inconsistent events.
- * Enable this via "adb shell setprop log.tag.InputVerifierLogEvents DEBUG"
- */
-static bool logEvents() {
-    return __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG "LogEvents", ANDROID_LOG_INFO);
-}
-
 // --- InputVerifier ---
 
-InputVerifier::InputVerifier(const std::string& name) : mName(name){};
+InputVerifier::InputVerifier(const std::string& name)
+      : mVerifier(android::input::verifier::create(name)){};
 
-void InputVerifier::processMovement(int32_t deviceId, int32_t action, uint32_t pointerCount,
-                                    const PointerProperties* pointerProperties,
-                                    const PointerCoords* pointerCoords, int32_t flags) {
-    if (logEvents()) {
-        LOG(ERROR) << "Processing " << MotionEvent::actionToString(action) << " for device "
-                   << deviceId << " (" << pointerCount << " pointer"
-                   << (pointerCount == 1 ? "" : "s") << ") on " << mName;
+Result<void> InputVerifier::processMovement(int32_t deviceId, int32_t action, uint32_t pointerCount,
+                                            const PointerProperties* pointerProperties,
+                                            const PointerCoords* pointerCoords, int32_t flags) {
+    std::vector<RustPointerProperties> rpp;
+    for (size_t i = 0; i < pointerCount; i++) {
+        rpp.emplace_back(RustPointerProperties{.id = pointerProperties[i].id});
     }
-
-    switch (MotionEvent::getActionMasked(action)) {
-        case AMOTION_EVENT_ACTION_DOWN: {
-            auto [it, inserted] = mTouchingPointerIdsByDevice.insert({deviceId, {}});
-            if (!inserted) {
-                LOG(FATAL) << "Got ACTION_DOWN, but already have touching pointers " << it->second
-                           << " for device " << deviceId << " on " << mName;
-            }
-            it->second.set(pointerProperties[0].id);
-            break;
-        }
-        case AMOTION_EVENT_ACTION_POINTER_DOWN: {
-            auto it = mTouchingPointerIdsByDevice.find(deviceId);
-            if (it == mTouchingPointerIdsByDevice.end()) {
-                LOG(FATAL) << "Got POINTER_DOWN, but no touching pointers for device " << deviceId
-                           << " on " << mName;
-            }
-            it->second.set(pointerProperties[MotionEvent::getActionIndex(action)].id);
-            break;
-        }
-        case AMOTION_EVENT_ACTION_MOVE: {
-            ensureTouchingPointersMatch(deviceId, pointerCount, pointerProperties, "MOVE");
-            break;
-        }
-        case AMOTION_EVENT_ACTION_POINTER_UP: {
-            auto it = mTouchingPointerIdsByDevice.find(deviceId);
-            if (it == mTouchingPointerIdsByDevice.end()) {
-                LOG(FATAL) << "Got POINTER_UP, but no touching pointers for device " << deviceId
-                           << " on " << mName;
-            }
-            it->second.reset(pointerProperties[MotionEvent::getActionIndex(action)].id);
-            break;
-        }
-        case AMOTION_EVENT_ACTION_UP: {
-            auto it = mTouchingPointerIdsByDevice.find(deviceId);
-            if (it == mTouchingPointerIdsByDevice.end()) {
-                LOG(FATAL) << "Got ACTION_UP, but no record for deviceId " << deviceId << " on "
-                           << mName;
-            }
-            const auto& [_, touchingPointerIds] = *it;
-            if (touchingPointerIds.count() != 1) {
-                LOG(FATAL) << "Got ACTION_UP, but we have pointers: " << touchingPointerIds
-                           << " for deviceId " << deviceId << " on " << mName;
-            }
-            const int32_t pointerId = pointerProperties[0].id;
-            if (!touchingPointerIds.test(pointerId)) {
-                LOG(FATAL) << "Got ACTION_UP, but pointerId " << pointerId
-                           << " is not touching. Touching pointers: " << touchingPointerIds
-                           << " for deviceId " << deviceId << " on " << mName;
-            }
-            mTouchingPointerIdsByDevice.erase(it);
-            break;
-        }
-        case AMOTION_EVENT_ACTION_CANCEL: {
-            if ((flags & AMOTION_EVENT_FLAG_CANCELED) != AMOTION_EVENT_FLAG_CANCELED) {
-                LOG(FATAL) << "For ACTION_CANCEL, must set FLAG_CANCELED";
-            }
-            ensureTouchingPointersMatch(deviceId, pointerCount, pointerProperties, "CANCEL");
-            mTouchingPointerIdsByDevice.erase(deviceId);
-            break;
-        }
+    rust::Slice<const RustPointerProperties> properties{rpp.data(), rpp.size()};
+    rust::String errorMessage =
+            android::input::verifier::process_movement(*mVerifier, deviceId, action, properties,
+                                                       flags);
+    if (errorMessage.empty()) {
+        return {};
+    } else {
+        return Error() << errorMessage;
     }
 }
 
-void InputVerifier::ensureTouchingPointersMatch(int32_t deviceId, uint32_t pointerCount,
-                                                const PointerProperties* pointerProperties,
-                                                const char* action) const {
-    auto it = mTouchingPointerIdsByDevice.find(deviceId);
-    if (it == mTouchingPointerIdsByDevice.end()) {
-        LOG(FATAL) << "Got " << action << ", but no touching pointers for device " << deviceId
-                   << " on " << mName;
-    }
-    const auto& [_, touchingPointerIds] = *it;
-    for (size_t i = 0; i < pointerCount; i++) {
-        const int32_t pointerId = pointerProperties[i].id;
-        if (!touchingPointerIds.test(pointerId)) {
-            LOG(FATAL) << "Got " << action << " for pointerId " << pointerId
-                       << " but the touching pointers are " << touchingPointerIds << " on "
-                       << mName;
-        }
-    }
-};
-
 } // namespace android
diff --git a/libs/input/InputWrapper.hpp b/libs/input/InputWrapper.hpp
new file mode 100644
index 0000000..a01080d
--- /dev/null
+++ b/libs/input/InputWrapper.hpp
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2023 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 <android/input.h>
+#include "input/Input.h"
diff --git a/libs/input/ffi/FromRustToCpp.h b/libs/input/ffi/FromRustToCpp.h
new file mode 100644
index 0000000..889945c
--- /dev/null
+++ b/libs/input/ffi/FromRustToCpp.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 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 "rust/cxx.h"
+
+namespace android {
+
+bool shouldLog(rust::Str tag);
+
+} // namespace android
diff --git a/libs/input/input_verifier.rs b/libs/input/input_verifier.rs
new file mode 100644
index 0000000..2e05a63
--- /dev/null
+++ b/libs/input/input_verifier.rs
@@ -0,0 +1,421 @@
+/*
+ * Copyright 2023 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.
+ */
+
+//! Validate the incoming motion stream.
+//! This class is not thread-safe.
+//! State is stored in the "InputVerifier" object
+//! that can be created via the 'create' method.
+//! Usage:
+//! Box<InputVerifier> verifier = create("inputChannel name");
+//! result = process_movement(verifier, ...);
+//! if (result) {
+//!    crash(result.error_message());
+//! }
+
+use std::collections::HashMap;
+use std::collections::HashSet;
+
+use bitflags::bitflags;
+use log::info;
+
+#[cxx::bridge(namespace = "android::input")]
+mod ffi {
+    #[namespace = "android"]
+    unsafe extern "C++" {
+        include!("ffi/FromRustToCpp.h");
+        fn shouldLog(tag: &str) -> bool;
+    }
+    #[namespace = "android::input::verifier"]
+    extern "Rust" {
+        type InputVerifier;
+
+        fn create(name: String) -> Box<InputVerifier>;
+        fn process_movement(
+            verifier: &mut InputVerifier,
+            device_id: i32,
+            action: u32,
+            pointer_properties: &[RustPointerProperties],
+            flags: i32,
+        ) -> String;
+    }
+
+    pub struct RustPointerProperties {
+        id: i32,
+    }
+}
+
+use crate::ffi::shouldLog;
+use crate::ffi::RustPointerProperties;
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+struct DeviceId(i32);
+
+fn process_movement(
+    verifier: &mut InputVerifier,
+    device_id: i32,
+    action: u32,
+    pointer_properties: &[RustPointerProperties],
+    flags: i32,
+) -> String {
+    let result = verifier.process_movement(
+        DeviceId(device_id),
+        action,
+        pointer_properties,
+        Flags::from_bits(flags).unwrap(),
+    );
+    match result {
+        Ok(()) => "".to_string(),
+        Err(e) => e,
+    }
+}
+
+fn create(name: String) -> Box<InputVerifier> {
+    Box::new(InputVerifier::new(&name))
+}
+
+#[repr(u32)]
+enum MotionAction {
+    Down = input_bindgen::AMOTION_EVENT_ACTION_DOWN,
+    Up = input_bindgen::AMOTION_EVENT_ACTION_UP,
+    Move = input_bindgen::AMOTION_EVENT_ACTION_MOVE,
+    Cancel = input_bindgen::AMOTION_EVENT_ACTION_CANCEL,
+    Outside = input_bindgen::AMOTION_EVENT_ACTION_OUTSIDE,
+    PointerDown { action_index: usize } = input_bindgen::AMOTION_EVENT_ACTION_POINTER_DOWN,
+    PointerUp { action_index: usize } = input_bindgen::AMOTION_EVENT_ACTION_POINTER_UP,
+    HoverEnter = input_bindgen::AMOTION_EVENT_ACTION_HOVER_ENTER,
+    HoverMove = input_bindgen::AMOTION_EVENT_ACTION_HOVER_MOVE,
+    HoverExit = input_bindgen::AMOTION_EVENT_ACTION_HOVER_EXIT,
+    Scroll = input_bindgen::AMOTION_EVENT_ACTION_SCROLL,
+    ButtonPress = input_bindgen::AMOTION_EVENT_ACTION_BUTTON_PRESS,
+    ButtonRelease = input_bindgen::AMOTION_EVENT_ACTION_BUTTON_RELEASE,
+}
+
+fn get_action_index(action: u32) -> usize {
+    let index = (action & input_bindgen::AMOTION_EVENT_ACTION_POINTER_INDEX_MASK)
+        >> input_bindgen::AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
+    index.try_into().unwrap()
+}
+
+impl From<u32> for MotionAction {
+    fn from(action: u32) -> Self {
+        let action_masked = action & input_bindgen::AMOTION_EVENT_ACTION_MASK;
+        let action_index = get_action_index(action);
+        match action_masked {
+            input_bindgen::AMOTION_EVENT_ACTION_DOWN => MotionAction::Down,
+            input_bindgen::AMOTION_EVENT_ACTION_UP => MotionAction::Up,
+            input_bindgen::AMOTION_EVENT_ACTION_MOVE => MotionAction::Move,
+            input_bindgen::AMOTION_EVENT_ACTION_CANCEL => MotionAction::Cancel,
+            input_bindgen::AMOTION_EVENT_ACTION_OUTSIDE => MotionAction::Outside,
+            input_bindgen::AMOTION_EVENT_ACTION_POINTER_DOWN => {
+                MotionAction::PointerDown { action_index }
+            }
+            input_bindgen::AMOTION_EVENT_ACTION_POINTER_UP => {
+                MotionAction::PointerUp { action_index }
+            }
+            input_bindgen::AMOTION_EVENT_ACTION_HOVER_ENTER => MotionAction::HoverEnter,
+            input_bindgen::AMOTION_EVENT_ACTION_HOVER_MOVE => MotionAction::HoverMove,
+            input_bindgen::AMOTION_EVENT_ACTION_HOVER_EXIT => MotionAction::HoverExit,
+            input_bindgen::AMOTION_EVENT_ACTION_SCROLL => MotionAction::Scroll,
+            input_bindgen::AMOTION_EVENT_ACTION_BUTTON_PRESS => MotionAction::ButtonPress,
+            input_bindgen::AMOTION_EVENT_ACTION_BUTTON_RELEASE => MotionAction::ButtonRelease,
+            _ => panic!("Unknown action: {}", action),
+        }
+    }
+}
+
+bitflags! {
+    struct Flags: i32 {
+        const CANCELED = input_bindgen::AMOTION_EVENT_FLAG_CANCELED;
+    }
+}
+
+fn motion_action_to_string(action: u32) -> String {
+    match action.into() {
+        MotionAction::Down => "DOWN".to_string(),
+        MotionAction::Up => "UP".to_string(),
+        MotionAction::Move => "MOVE".to_string(),
+        MotionAction::Cancel => "CANCEL".to_string(),
+        MotionAction::Outside => "OUTSIDE".to_string(),
+        MotionAction::PointerDown { action_index } => {
+            format!("POINTER_DOWN({})", action_index)
+        }
+        MotionAction::PointerUp { action_index } => {
+            format!("POINTER_UP({})", action_index)
+        }
+        MotionAction::HoverMove => "HOVER_MOVE".to_string(),
+        MotionAction::Scroll => "SCROLL".to_string(),
+        MotionAction::HoverEnter => "HOVER_ENTER".to_string(),
+        MotionAction::HoverExit => "HOVER_EXIT".to_string(),
+        MotionAction::ButtonPress => "BUTTON_PRESS".to_string(),
+        MotionAction::ButtonRelease => "BUTTON_RELEASE".to_string(),
+    }
+}
+
+/**
+ * Log all of the movements that are sent to this verifier. Helps to identify the streams that lead
+ * to inconsistent events.
+ * Enable this via "adb shell setprop log.tag.InputVerifierLogEvents DEBUG"
+ */
+fn log_events() -> bool {
+    shouldLog("InputVerifierLogEvents")
+}
+
+struct InputVerifier {
+    name: String,
+    touching_pointer_ids_by_device: HashMap<DeviceId, HashSet<i32>>,
+}
+
+impl InputVerifier {
+    fn new(name: &str) -> Self {
+        logger::init(
+            logger::Config::default()
+                .with_tag_on_device("InputVerifier")
+                .with_min_level(log::Level::Trace),
+        );
+        Self { name: name.to_owned(), touching_pointer_ids_by_device: HashMap::new() }
+    }
+
+    fn process_movement(
+        &mut self,
+        device_id: DeviceId,
+        action: u32,
+        pointer_properties: &[RustPointerProperties],
+        flags: Flags,
+    ) -> Result<(), String> {
+        if log_events() {
+            info!(
+                "Processing {} for device {:?} ({} pointer{}) on {}",
+                motion_action_to_string(action),
+                device_id,
+                pointer_properties.len(),
+                if pointer_properties.len() == 1 { "" } else { "s" },
+                self.name
+            );
+        }
+
+        match action.into() {
+            MotionAction::Down => {
+                let it = self
+                    .touching_pointer_ids_by_device
+                    .entry(device_id)
+                    .or_insert_with(HashSet::new);
+                let pointer_id = pointer_properties[0].id;
+                if it.contains(&pointer_id) {
+                    return Err(format!(
+                        "{}: Invalid DOWN event - pointers already down for device {:?}: {:?}",
+                        self.name, device_id, it
+                    ));
+                }
+                it.insert(pointer_id);
+            }
+            MotionAction::PointerDown { action_index } => {
+                if !self.touching_pointer_ids_by_device.contains_key(&device_id) {
+                    return Err(format!(
+                        "{}: Received POINTER_DOWN but no pointers are currently down \
+                        for device {:?}",
+                        self.name, device_id
+                    ));
+                }
+                let it = self.touching_pointer_ids_by_device.get_mut(&device_id).unwrap();
+                let pointer_id = pointer_properties[action_index].id;
+                if it.contains(&pointer_id) {
+                    return Err(format!(
+                        "{}: Pointer with id={} not found in the properties",
+                        self.name, pointer_id
+                    ));
+                }
+                it.insert(pointer_id);
+            }
+            MotionAction::Move => {
+                if !self.ensure_touching_pointers_match(device_id, pointer_properties) {
+                    return Err(format!(
+                        "{}: ACTION_MOVE touching pointers don't match",
+                        self.name
+                    ));
+                }
+            }
+            MotionAction::PointerUp { action_index } => {
+                if !self.touching_pointer_ids_by_device.contains_key(&device_id) {
+                    return Err(format!(
+                        "{}: Received POINTER_UP but no pointers are currently down for device \
+                        {:?}",
+                        self.name, device_id
+                    ));
+                }
+                let it = self.touching_pointer_ids_by_device.get_mut(&device_id).unwrap();
+                let pointer_id = pointer_properties[action_index].id;
+                it.remove(&pointer_id);
+            }
+            MotionAction::Up => {
+                if !self.touching_pointer_ids_by_device.contains_key(&device_id) {
+                    return Err(format!(
+                        "{} Received ACTION_UP but no pointers are currently down for device {:?}",
+                        self.name, device_id
+                    ));
+                }
+                let it = self.touching_pointer_ids_by_device.get_mut(&device_id).unwrap();
+                if it.len() != 1 {
+                    return Err(format!(
+                        "{}: Got ACTION_UP, but we have pointers: {:?} for device {:?}",
+                        self.name, it, device_id
+                    ));
+                }
+                let pointer_id = pointer_properties[0].id;
+                if !it.contains(&pointer_id) {
+                    return Err(format!(
+                        "{}: Got ACTION_UP, but pointerId {} is not touching. Touching pointers:\
+                        {:?} for device {:?}",
+                        self.name, pointer_id, it, device_id
+                    ));
+                }
+                it.clear();
+            }
+            MotionAction::Cancel => {
+                if flags.contains(Flags::CANCELED) {
+                    return Err(format!(
+                        "{}: For ACTION_CANCEL, must set FLAG_CANCELED",
+                        self.name
+                    ));
+                }
+                if !self.ensure_touching_pointers_match(device_id, pointer_properties) {
+                    return Err(format!(
+                        "{}: Got ACTION_CANCEL, but the pointers don't match. \
+                        Existing pointers: {:?}",
+                        self.name, self.touching_pointer_ids_by_device
+                    ));
+                }
+                self.touching_pointer_ids_by_device.remove(&device_id);
+            }
+            _ => return Ok(()),
+        }
+        Ok(())
+    }
+
+    fn ensure_touching_pointers_match(
+        &self,
+        device_id: DeviceId,
+        pointer_properties: &[RustPointerProperties],
+    ) -> bool {
+        let Some(pointers) = self.touching_pointer_ids_by_device.get(&device_id) else {
+            return false;
+        };
+
+        for pointer_property in pointer_properties.iter() {
+            let pointer_id = pointer_property.id;
+            if !pointers.contains(&pointer_id) {
+                return false;
+            }
+        }
+        true
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::DeviceId;
+    use crate::Flags;
+    use crate::InputVerifier;
+    use crate::RustPointerProperties;
+    #[test]
+    fn single_pointer_stream() {
+        let mut verifier = InputVerifier::new("Test");
+        let pointer_properties = Vec::from([RustPointerProperties { id: 0 }]);
+        assert!(verifier
+            .process_movement(
+                DeviceId(1),
+                input_bindgen::AMOTION_EVENT_ACTION_DOWN,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_ok());
+        assert!(verifier
+            .process_movement(
+                DeviceId(1),
+                input_bindgen::AMOTION_EVENT_ACTION_MOVE,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_ok());
+        assert!(verifier
+            .process_movement(
+                DeviceId(1),
+                input_bindgen::AMOTION_EVENT_ACTION_UP,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_ok());
+    }
+
+    #[test]
+    fn multi_device_stream() {
+        let mut verifier = InputVerifier::new("Test");
+        let pointer_properties = Vec::from([RustPointerProperties { id: 0 }]);
+        assert!(verifier
+            .process_movement(
+                DeviceId(1),
+                input_bindgen::AMOTION_EVENT_ACTION_DOWN,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_ok());
+        assert!(verifier
+            .process_movement(
+                DeviceId(1),
+                input_bindgen::AMOTION_EVENT_ACTION_MOVE,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_ok());
+        assert!(verifier
+            .process_movement(
+                DeviceId(2),
+                input_bindgen::AMOTION_EVENT_ACTION_DOWN,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_ok());
+        assert!(verifier
+            .process_movement(
+                DeviceId(2),
+                input_bindgen::AMOTION_EVENT_ACTION_MOVE,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_ok());
+        assert!(verifier
+            .process_movement(
+                DeviceId(1),
+                input_bindgen::AMOTION_EVENT_ACTION_UP,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_ok());
+    }
+
+    #[test]
+    fn test_invalid_up() {
+        let mut verifier = InputVerifier::new("Test");
+        let pointer_properties = Vec::from([RustPointerProperties { id: 0 }]);
+        assert!(verifier
+            .process_movement(
+                DeviceId(1),
+                input_bindgen::AMOTION_EVENT_ACTION_UP,
+                &pointer_properties,
+                Flags::empty(),
+            )
+            .is_err());
+    }
+}