[input_sample] Sample Input Driver Using input_reports_reader

Sample Input Driver implementing fuchsia.input.report which uses
input_reports_reader to send fake mouse reports every 1 second.

Test: tested on QEMU that we can use `ffx driver print-input-report
read` to read the fake reports from sample driver.
      bazel run --config=fuchsia_x64 src/input_sample:pkg.component
      tools/ffx driver print-input-report descriptor
      tools/ffx driver print-input-report read

Bug: 103070
Change-Id: I3eeb571f044e4570aa93c16607e5def0a30134ef
Reviewed-on: https://fuchsia-review.googlesource.com/c/sdk-samples/drivers/+/692855
Reviewed-by: Suraj Malhotra <surajmalhotra@google.com>
Commit-Queue: Ruby Zhuang <rdzhuang@google.com>
Reviewed-by: David Gilhooley <dgilhooley@google.com>
diff --git a/src/input_sample/BUILD.bazel b/src/input_sample/BUILD.bazel
new file mode 100644
index 0000000..36436f3
--- /dev/null
+++ b/src/input_sample/BUILD.bazel
@@ -0,0 +1,57 @@
+load(
+    "@rules_fuchsia//fuchsia:defs.bzl",
+    "fuchsia_component_manifest",
+    "fuchsia_driver_bytecode_bind_rules",
+    "fuchsia_driver_component",
+    "fuchsia_package",
+)
+
+fuchsia_driver_bytecode_bind_rules(
+    name = "bind_bytecode",
+    output = "input_sample.bindbc",
+    rules = "input_sample.bind",
+    deps = [ "@fuchsia_sdk//bind/fuchsia.acpi" ],
+)
+
+cc_binary(
+    name = "input_sample",
+    srcs = [
+        "input_sample.cc",
+        "input_sample.h",
+    ],
+    linkshared = True,
+    deps = [
+        "@fuchsia_sdk//fidl/fuchsia.device.fs:fuchsia.device.fs_llcpp_cc",
+        "@fuchsia_sdk//fidl/fuchsia.driver.compat:fuchsia.driver.compat_llcpp_cc",
+        "@fuchsia_sdk//fidl/fuchsia.input.report:fuchsia.input.report_llcpp_cc",
+        "@fuchsia_sdk//fidl/zx:zx_cc",
+        "@fuchsia_sdk//pkg/async-cpp",
+        "@fuchsia_sdk//pkg/driver2-llcpp",
+        "@fuchsia_sdk//pkg/driver_runtime_cpp",
+        "@fuchsia_sdk//pkg/fidl-llcpp",
+        "@fuchsia_sdk//pkg/input_report_reader",
+        "@fuchsia_sdk//pkg/sys_component_llcpp",
+        "@fuchsia_sdk//pkg/zx",
+    ],
+)
+
+fuchsia_component_manifest(
+    name = "manifest",
+    src = "meta/input_sample.cml",
+)
+
+fuchsia_driver_component(
+    name = "component",
+    bind_bytecode = ":bind_bytecode",
+    driver_lib = ":input_sample",
+    manifest = ":manifest",
+)
+
+fuchsia_package(
+    name = "pkg",
+    package_name = "input_sample",
+    visibility = ["//visibility:public"],
+    deps = [
+        ":component",
+    ],
+)
diff --git a/src/input_sample/README.md b/src/input_sample/README.md
new file mode 100644
index 0000000..4ad8d5d
--- /dev/null
+++ b/src/input_sample/README.md
@@ -0,0 +1,107 @@
+# Input Driver Sample using Fuchsia SDK
+
+This sample input driver implements fuchsia.input.report using the input_reports_reader SDK  to send fake mouse reports every 1 second.
+
+## Run and test driver
+
+After starting up the emulator
+
+1. Load the sample driver
+
+   ```
+   bazel run --config=fuchsia_x64 src/input_sample:pkg.component
+   ```
+
+2. Read the fake input mouse descriptor
+
+   ```
+   tools/ffx driver print-input-report descriptor
+   ```
+
+   This should return a list of descriptors. The fake mouse descriptor for this driver is
+   
+   ```
+   Descriptor from file: "class/input-report/002"
+   {
+      device_info: {
+         vendor_id: 305419896
+         product_id: 2271560481
+         version: 0
+      }
+      mouse: {
+         input: {
+            movement_x: {
+               range: {
+                  min: -127
+                  max: 127
+               }
+               unit: {
+                  type: None
+                  exponent: 0
+               }
+            }
+            movement_y: {
+               range: {
+                  min: -127
+                  max: 127
+               }
+               unit: {
+                  type: None
+                  exponent: 0
+               }
+            }
+            scroll_v: None
+            scroll_h: None
+            buttons: [1, 2, 3]
+            position_x: None
+            position_y: None
+         }
+      }
+      sensor: None
+      touch: None
+      keyboard: None
+      consumer_control: None
+   }
+   ```
+
+   In particular, it should be the one with
+
+   ```
+   vendor_id: 305419896     // 0x12345678
+   product_id: 2271560481   // 0x87654321
+   ```
+
+   You may exit this with CTRL-C.
+
+2. Read the fake input mouse reports
+
+   ```
+   tools/ffx driver print-input-report read
+   ```
+
+   This will produce logs that look like
+   
+   ```
+   {
+     event_time: 335266867991
+     trace_id: 9
+     report_id: None
+     mouse: {
+        movement_x: 29
+        movement_y: 29
+        scroll_v: None
+        scroll_h: None
+        pressed_buttons: [1, 3]
+        position_x: None
+        position_y: None
+     }
+     sensor: None
+     touch: None
+     keyboard: None
+     consumer_control: None
+   }
+   ```
+
+   once a second. Each second movement_x and movement_y should increment, pressed_buttons should change, and event_time should also change.
+   
+   You may exit this with CTRL-C.
diff --git a/src/input_sample/input_sample.bind b/src/input_sample/input_sample.bind
new file mode 100644
index 0000000..423c152
--- /dev/null
+++ b/src/input_sample/input_sample.bind
@@ -0,0 +1,14 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+using fuchsia.acpi;
+
+// Bind to an unused ACPI node in Qemu or Atlas.
+// TODO(fxb/100716): Bind to a test node once it's available.
+if fuchsia.acpi.hid == "QEMU0002" {
+    true;
+} else {
+    fuchsia.acpi.hid == "PNP0C02";
+}
+
diff --git a/src/input_sample/input_sample.cc b/src/input_sample/input_sample.cc
new file mode 100644
index 0000000..de2f5df
--- /dev/null
+++ b/src/input_sample/input_sample.cc
@@ -0,0 +1,213 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/input_sample/input_sample.h"
+
+#include <fidl/fuchsia.component.decl/cpp/wire.h>
+#include <fidl/fuchsia.driver.compat/cpp/wire.h>
+#include <lib/async/cpp/task.h>
+#include <lib/driver2/record_cpp.h>
+
+#include <unistd.h>
+
+namespace fdf {
+using namespace fuchsia_driver_framework;
+}  // namespace fdf
+
+namespace input_sample {
+
+namespace {
+
+namespace fcd = fuchsia_component_decl;
+namespace fio = fuchsia_io;
+
+zx::status<fidl::ClientEnd<fuchsia_driver_compat::Device>> ConnectToParentDevice(
+    const driver::Namespace* ns, std::string_view name) {
+  auto result = ns->OpenService<fuchsia_driver_compat::Service>(name);
+  if (result.is_error()) {
+    return result.take_error();
+  }
+  return result.value().connect_device();
+}
+
+constexpr uint8_t kMouseButtonCount = 3;
+
+}  // namespace
+
+void SampleInputReport::ToFidlInputReport(
+    fidl::WireTableBuilder<::fuchsia_input_report::wire::InputReport>& input_report,
+    fidl::AnyArena& arena) {
+  std::vector<uint8_t> pressed_buttons;
+  for (uint8_t i = 0; i < kMouseButtonCount; i++) {
+    if (buttons & (1 << i)) {
+      pressed_buttons.push_back(i + 1);
+    }
+  }
+  fidl::VectorView<uint8_t> buttons_vec(arena, pressed_buttons.size());
+  size_t idx = 0;
+  for (const auto& button : pressed_buttons) {
+    buttons_vec[idx++] = button;
+  }
+
+  auto mouse_input_rpt = fuchsia_input_report::wire::MouseInputReport::Builder(arena);
+  mouse_input_rpt.pressed_buttons(buttons_vec);
+  mouse_input_rpt.movement_x(rel_x);
+  mouse_input_rpt.movement_y(rel_y);
+
+  input_report.mouse(mouse_input_rpt.Build());
+  input_report.event_time(event_time.get());
+}
+
+void InputSampleDriver::SendFakeReport() {
+  // Update fake mouse report
+  report_.event_time = zx::time(zx_clock_get_monotonic());
+  report_.buttons++;
+  report_.rel_x++;
+  report_.rel_y++;
+
+  // Send fake mouse report
+  input_report_readers_.SendReportToAllReaders(report_);
+
+  // Run again in 1 second
+  async::PostDelayedTask(
+      dispatcher_,
+      [this]() mutable {
+        SendFakeReport();
+      },
+      zx::sec(1));
+}
+
+// static
+zx::status<std::unique_ptr<InputSampleDriver>> InputSampleDriver::Start(
+    fdf::wire::DriverStartArgs& start_args, fdf::UnownedDispatcher dispatcher,
+    fidl::WireSharedClient<fdf::Node> node, driver::Namespace ns, driver::Logger logger) {
+  auto driver = std::make_unique<InputSampleDriver>(dispatcher->async_dispatcher(), std::move(node),
+                                                    std::move(ns), std::move(logger));
+  auto result = driver->Run(std::move(start_args.outgoing_dir()));
+  if (result.is_error()) {
+    return result.take_error();
+  }
+  return zx::ok(std::move(driver));
+}
+
+zx::status<> InputSampleDriver::Run(fidl::ServerEnd<fio::Directory> outgoing_dir) {
+  // Connect to DevfsExporter.
+  auto exporter = ns_.Connect<fuchsia_device_fs::Exporter>();
+  if (exporter.status_value() != ZX_OK) {
+    return exporter.take_error();
+  }
+  exporter_ = fidl::WireClient(std::move(*exporter), dispatcher_);
+
+  // Connect to parent.
+  auto parent = ConnectToParentDevice(&ns_, "default");
+  if (parent.status_value() != ZX_OK) {
+    FDF_SLOG(ERROR, "Failed to connect to parent", KV("status", parent.status_string()));
+    return parent.take_error();
+  }
+
+  auto result = fidl::WireCall(*parent)->GetTopologicalPath();
+  if (!result.ok()) {
+    return zx::error(result.status());
+  }
+
+  std::string path(result->path.data(), result->path.size());
+
+  // TODO (fxbug.dev/103199): migrate off this when fuchsia.input.report InputDevice is discoverable
+  auto status = outgoing_.AddProtocol<fuchsia_input_report::InputDevice>(this, Name());
+  if (status.status_value() != ZX_OK) {
+    return status;
+  }
+
+  path.append("/");
+  path.append(Name());
+
+  FDF_LOG(INFO, "Exporting device to: %s", path.data());
+
+  // Serve a connection to outgoing.
+  auto endpoints = fidl::CreateEndpoints<fuchsia_io::Directory>();
+  if (endpoints.is_error()) {
+    return endpoints.take_error();
+  }
+  {
+    auto status = outgoing_.Serve(std::move(endpoints->server));
+    if (status.is_error()) {
+      return status.take_error();
+    }
+  }
+
+  // Export our protocol.
+  exporter_
+      ->Export(std::move(endpoints->client),
+               // TODO (fxbug.dev/103199): migrate off this when fuchsia.input.report
+               //                          InputDevice is discoverable
+               fidl::StringView::FromExternal(std::string("svc/").append(Name())),
+               fidl::StringView::FromExternal(path),
+               /* INPUT_REPORT */ 26)  // TODO (fxbug.dev/98831): Non-hardcoded proto_id
+      .Then([this](fidl::WireUnownedResult<fuchsia_device_fs::Exporter::Export>& result) {
+        if (!result.ok()) {
+          FDF_LOG(ERROR, "Exporting failed with: %s", result.status_string());
+        }
+      });
+
+  status = outgoing_.Serve(std::move(outgoing_dir));
+  if (status.is_error()) {
+    FDF_LOG(ERROR, "Could not serve: %d", status.error_value());
+    return status;
+  }
+
+  // Start sending fake reports
+  SendFakeReport();
+
+  return zx::ok();
+}
+
+void InputSampleDriver::GetInputReportsReader(GetInputReportsReaderRequestView request,
+                                              GetInputReportsReaderCompleter::Sync& completer) {
+  zx_status_t status = input_report_readers_.CreateReader(dispatcher_, std::move(request->reader));
+  if (status != ZX_OK) {
+    FDF_LOG(ERROR, "Could not create input report reader: %d", status);
+  }
+}
+
+void InputSampleDriver::GetDescriptor(GetDescriptorRequestView request,
+                                      GetDescriptorCompleter::Sync& completer) {
+  fidl::Arena allocator;
+  auto descriptor = fuchsia_input_report::wire::DeviceDescriptor::Builder(allocator);
+
+  fuchsia_input_report::wire::DeviceInfo device_info;
+  // We pick random VID/PIDs here, but these should be taken from the HID device,
+  // or defined in fuchsia.input.report/device_ids.fidl
+  device_info.vendor_id = 0x12345678;
+  device_info.product_id = 0x87654321;
+
+  fidl::VectorView<uint8_t> buttons(allocator, kMouseButtonCount);
+  buttons[0] = 0x01;
+  buttons[1] = 0x02;
+  buttons[2] = 0x03;
+
+  constexpr fuchsia_input_report::wire::Axis movement_x{
+      .range = {.min = -127, .max = 127},
+      .unit = {.type = fuchsia_input_report::wire::UnitType::kNone, .exponent = 0},
+  };
+  constexpr fuchsia_input_report::wire::Axis movement_y{
+      .range = {.min = -127, .max = 127},
+      .unit = {.type = fuchsia_input_report::wire::UnitType::kNone, .exponent = 0},
+  };
+
+  auto mouse_in_desc = fuchsia_input_report::wire::MouseInputDescriptor::Builder(allocator);
+  mouse_in_desc.buttons(buttons);
+  mouse_in_desc.movement_x(movement_x);
+  mouse_in_desc.movement_y(movement_y);
+
+  auto mouse_descriptor = fuchsia_input_report::wire::MouseDescriptor::Builder(allocator);
+  mouse_descriptor.input(mouse_in_desc.Build());
+  descriptor.mouse(mouse_descriptor.Build());
+  descriptor.device_info(device_info);
+
+  completer.Reply(descriptor.Build());
+}
+
+}  // namespace input_sample
+
+FUCHSIA_DRIVER_RECORD_CPP_V1(input_sample::InputSampleDriver);
diff --git a/src/input_sample/input_sample.h b/src/input_sample/input_sample.h
new file mode 100644
index 0000000..9870eb7
--- /dev/null
+++ b/src/input_sample/input_sample.h
@@ -0,0 +1,94 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef FUCHSIA_SDK_EXAMPLES_CC_INPUT_SAMPLE_H_
+#define FUCHSIA_SDK_EXAMPLES_CC_INPUT_SAMPLE_H_
+
+#include <fidl/fuchsia.device.fs/cpp/wire.h>
+#include <fidl/fuchsia.driver.framework/cpp/wire.h>
+#include <fidl/fuchsia.input.report/cpp/wire.h>
+#include <lib/async/dispatcher.h>
+#include <lib/driver2/namespace.h>
+#include <lib/driver2/structured_logger.h>
+#include <lib/fdf/cpp/dispatcher.h>
+#include <lib/input_report_reader/reader.h>
+#include <lib/sys/component/llcpp/outgoing_directory.h>
+#include <lib/zx/status.h>
+
+#include <memory>
+#include <mutex>
+
+namespace input_sample {
+
+struct SampleInputReport {
+  zx::time event_time;
+  uint8_t buttons = 0;
+  int8_t rel_x = 0;
+  int8_t rel_y = 0;
+
+  void ToFidlInputReport(
+      fidl::WireTableBuilder<::fuchsia_input_report::wire::InputReport>& input_report,
+      fidl::AnyArena& arena);
+};
+
+
+class InputSampleDriver : public fidl::WireServer<fuchsia_input_report::InputDevice> {
+ public:
+  InputSampleDriver(async_dispatcher_t* dispatcher,
+                fidl::WireSharedClient<fuchsia_driver_framework::Node> node, driver::Namespace ns,
+                driver::Logger logger)
+      : dispatcher_(dispatcher),
+        outgoing_(component::OutgoingDirectory::Create(dispatcher)),
+        node_(std::move(node)),
+        ns_(std::move(ns)),
+        logger_(std::move(logger)) {}
+  virtual ~InputSampleDriver() = default;
+
+  static constexpr const char* Name() { return "input-sample"; }
+
+  static zx::status<std::unique_ptr<InputSampleDriver>> Start(
+      fuchsia_driver_framework::wire::DriverStartArgs& start_args,
+      fdf::UnownedDispatcher dispatcher,
+      fidl::WireSharedClient<fuchsia_driver_framework::Node> node, driver::Namespace ns,
+      driver::Logger logger);
+
+  void GetInputReportsReader(GetInputReportsReaderRequestView request,
+                             GetInputReportsReaderCompleter::Sync& completer) override;
+  void GetDescriptor(GetDescriptorRequestView request,
+                     GetDescriptorCompleter::Sync& completer) override;
+  void SendOutputReport(SendOutputReportRequestView request,
+                        SendOutputReportCompleter::Sync& completer) override {
+    completer.ReplyError(ZX_ERR_NOT_SUPPORTED);
+  }
+  void GetFeatureReport(GetFeatureReportRequestView request,
+                        GetFeatureReportCompleter::Sync& completer) override {
+    completer.ReplyError(ZX_ERR_NOT_SUPPORTED);
+  }
+  void SetFeatureReport(SetFeatureReportRequestView request,
+                        SetFeatureReportCompleter::Sync& completer) override {
+    completer.ReplyError(ZX_ERR_NOT_SUPPORTED);
+  }
+  void GetInputReport(GetInputReportRequestView request,
+                      GetInputReportCompleter::Sync& completer) override {
+    completer.ReplyError(ZX_ERR_NOT_SUPPORTED);
+  }
+
+ private:
+  zx::status<> Run(fidl::ServerEnd<fuchsia_io::Directory> outgoing_dir);
+  void SendFakeReport();
+
+  async_dispatcher_t* const dispatcher_;
+  component::OutgoingDirectory outgoing_;
+  fidl::WireSharedClient<fuchsia_driver_framework::Node> node_;
+  driver::Namespace ns_;
+  driver::Logger logger_;
+  fidl::WireClient<fuchsia_device_fs::Exporter> exporter_;
+
+  input_report_reader::InputReportReaderManager<SampleInputReport> input_report_readers_;
+  SampleInputReport report_;
+};
+
+}  // namespace input_sample
+
+#endif  // FUCHSIA_SDK_EXAMPLES_CC_INPUT_SAMPLE_H_
diff --git a/src/input_sample/meta/input_sample.cml b/src/input_sample/meta/input_sample.cml
new file mode 100644
index 0000000..b9061d0
--- /dev/null
+++ b/src/input_sample/meta/input_sample.cml
@@ -0,0 +1,29 @@
+{
+    program: {
+        runner: 'driver',
+        binary: 'lib/libinput_sample.so',
+        bind: 'meta/bind/input_sample.bindbc'
+    },
+    use: [
+        {
+            protocol: [
+              'fuchsia.logger.LogSink',
+              'fuchsia.device.fs.Exporter'
+            ],
+        },
+        {
+            directory: 'fuchsia.driver.compat.Service-default',
+            rights: ['rw*'],
+            path: '/fuchsia.driver.compat.Service/default',
+        }
+    ],
+    capabilities: [
+        { protocol: 'fuchsia.input.report.InputDevice' },
+    ],
+    expose: [
+        {
+            protocol: 'fuchsia.input.report.InputDevice',
+            from: 'self',
+        },
+    ],
+}