blob: 85a0f1882b3621756bdf5a595fbc111426e2e7bd [file] [log] [blame]
// Copyright 2017 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 "keyboard.h"
#include <fcntl.h>
#include <fuchsia/hardware/input/c/fidl.h>
#include <fuchsia/io/c/fidl.h>
#include <lib/fdio/cpp/caller.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/fd.h>
#include <lib/fdio/fdio.h>
#include <lib/fdio/io.h>
#include <lib/fdio/spawn.h>
#include <lib/fdio/watcher.h>
#include <lib/zx/channel.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <zircon/syscalls.h>
#include <utility>
#include <ddk/device.h>
#include <hid/hid.h>
#include <hid/usages.h>
#include "src/ui/lib/key_util/key_util.h"
namespace {
constexpr zx::duration kSlackDuration = zx::msec(1);
constexpr zx::duration kHighRepeatKeyFreq = zx::msec(50);
constexpr zx::duration kLowRepeatKeyFreq = zx::msec(250);
// TODO(dgilhooley): this global watcher is necessary because the ports we are using
// take a raw function pointer. I think once we move this library to libasync we can
// remove the global watcher and use lambdas.
KeyboardWatcher main_watcher;
zx_status_t keyboard_main_callback(port_handler_t* ph, zx_signals_t signals, uint32_t evt) {
return main_watcher.DirCallback(ph, signals, evt);
}
int modifiers_from_fuchsia_key(llcpp::fuchsia::ui::input2::Key key) {
switch (key) {
case llcpp::fuchsia::ui::input2::Key::LEFT_SHIFT:
return MOD_LSHIFT;
case llcpp::fuchsia::ui::input2::Key::RIGHT_SHIFT:
return MOD_RSHIFT;
case llcpp::fuchsia::ui::input2::Key::LEFT_ALT:
return MOD_LALT;
case llcpp::fuchsia::ui::input2::Key::RIGHT_ALT:
return MOD_RALT;
case llcpp::fuchsia::ui::input2::Key::LEFT_CTRL:
return MOD_LCTRL;
case llcpp::fuchsia::ui::input2::Key::RIGHT_CTRL:
return MOD_RCTRL;
default:
return 0;
}
}
int modifiers_from_keycode(uint8_t keycode) {
switch (keycode) {
case HID_USAGE_KEY_LEFT_SHIFT:
return MOD_LSHIFT;
case HID_USAGE_KEY_RIGHT_SHIFT:
return MOD_RSHIFT;
case HID_USAGE_KEY_LEFT_ALT:
return MOD_LALT;
case HID_USAGE_KEY_RIGHT_ALT:
return MOD_RALT;
case HID_USAGE_KEY_LEFT_CTRL:
return MOD_LCTRL;
case HID_USAGE_KEY_RIGHT_CTRL:
return MOD_RCTRL;
}
return 0;
}
bool keycode_is_modifier(uint8_t keycode) { return modifiers_from_keycode(keycode) != 0; }
bool is_key_in_set(llcpp::fuchsia::ui::input2::Key key,
const fidl::VectorView<llcpp::fuchsia::ui::input2::Key>& set) {
for (auto s : set) {
if (key == s) {
return true;
}
}
return false;
}
} // namespace
zx_status_t setup_keyboard_watcher(keypress_handler_t handler, bool repeat_keys) {
return main_watcher.Setup(handler, repeat_keys);
}
zx_status_t Keyboard::TimerCallback(zx_signals_t signals, uint32_t evt) {
handler_(repeating_key_, modifiers_);
// increase repeat rate if we're not yet at the fastest rate
if ((repeat_interval_ = repeat_interval_ * 3 / 4) < kHighRepeatKeyFreq) {
repeat_interval_ = kHighRepeatKeyFreq;
}
timer_.set(zx::deadline_after(repeat_interval_), kSlackDuration);
return ZX_OK;
}
void Keyboard::SetCapsLockLed(bool caps_lock) {
if (!keyboard_client_) {
return;
}
// Generate the OutputReport.
auto report_builder = llcpp::fuchsia::input::report::OutputReport::UnownedBuilder();
auto keyboard_report_builder =
llcpp::fuchsia::input::report::KeyboardOutputReport::UnownedBuilder();
fidl::VectorView<llcpp::fuchsia::input::report::LedType> led_view;
llcpp::fuchsia::input::report::LedType caps_led =
llcpp::fuchsia::input::report::LedType::CAPS_LOCK;
if (caps_lock) {
led_view =
fidl::VectorView<llcpp::fuchsia::input::report::LedType>(fidl::unowned_ptr(&caps_led), 1);
} else {
led_view = fidl::VectorView<llcpp::fuchsia::input::report::LedType>(nullptr, 0);
}
keyboard_report_builder.set_enabled_leds(fidl::unowned_ptr(&led_view));
llcpp::fuchsia::input::report::KeyboardOutputReport keyboard_report =
keyboard_report_builder.build();
report_builder.set_keyboard(fidl::unowned_ptr(&keyboard_report));
llcpp::fuchsia::input::report::InputDevice::ResultOf::SendOutputReport result =
keyboard_client_->SendOutputReport(report_builder.build());
}
// returns true if key was pressed and none were released
void Keyboard::ProcessInput(const ::llcpp::fuchsia::input::report::InputReport& report) {
if (!report.has_keyboard()) {
return;
}
// Check if the keyboard FIDL table contains the pressed_keys vector. This vector should
// always exist. If it doesn't there's an error. If no keys are pressed this vector
// exists but is size 0.
if (!report.keyboard().has_pressed_keys()) {
return;
}
fidl::VectorView last_pressed_keys(fidl::unowned_ptr(last_pressed_keys_.data()),
last_pressed_keys_size_);
// Process the released keys.
for (llcpp::fuchsia::ui::input2::Key prev_key : last_pressed_keys) {
if (!is_key_in_set(prev_key, report.keyboard().pressed_keys())) {
modifiers_ &= ~modifiers_from_fuchsia_key(prev_key);
uint32_t hid_prev_key =
*key_util::fuchsia_key_to_hid_key(static_cast<::fuchsia::ui::input2::Key>(prev_key));
if (repeat_enabled_ && is_repeating_ && (repeating_key_ == hid_prev_key)) {
is_repeating_ = false;
timer_.cancel();
}
}
}
// Process the pressed keys.
for (llcpp::fuchsia::ui::input2::Key key : report.keyboard().pressed_keys()) {
if (!is_key_in_set(key, last_pressed_keys)) {
modifiers_ |= modifiers_from_fuchsia_key(key);
if (key == llcpp::fuchsia::ui::input2::Key::CAPS_LOCK) {
modifiers_ ^= MOD_CAPSLOCK;
SetCapsLockLed(modifiers_ & MOD_CAPSLOCK);
}
uint32_t hid_key =
*key_util::fuchsia_key_to_hid_key(static_cast<::fuchsia::ui::input2::Key>(key));
if (repeat_enabled_ && !keycode_is_modifier(hid_key)) {
is_repeating_ = true;
repeat_interval_ = kLowRepeatKeyFreq;
timer_.cancel();
timer_.set(zx::deadline_after(repeat_interval_), kSlackDuration);
repeating_key_ = hid_key;
}
handler_(hid_key, modifiers_);
}
}
// Store the previous state.
size_t i = 0;
for (llcpp::fuchsia::ui::input2::Key key : report.keyboard().pressed_keys()) {
last_pressed_keys_[i++] = key;
}
last_pressed_keys_size_ = i;
}
Keyboard::~Keyboard() {
if (input_notifier_.func != nullptr) {
port_cancel(&port, &input_notifier_);
}
if (timer_notifier_.func != nullptr) {
port_cancel(&port, &timer_notifier_);
}
}
zx_status_t Keyboard::InputCallback(zx_signals_t signals, uint32_t evt) {
if (!(signals & DEV_STATE_READABLE)) {
return ZX_ERR_STOP;
}
llcpp::fuchsia::input::report::InputDevice::ResultOf::GetReports result =
keyboard_client_->GetReports();
if (result.status() != ZX_OK) {
return result.status();
}
for (auto& report : result->reports) {
ProcessInput(report);
}
return ZX_OK;
}
zx_status_t Keyboard::Setup(
llcpp::fuchsia::input::report::InputDevice::SyncClient keyboard_client) {
keyboard_client_ = std::move(keyboard_client);
// XXX - check for LEDS here.
zx_status_t status;
if ((status = zx::timer::create(ZX_TIMER_SLACK_LATE, ZX_CLOCK_MONOTONIC, &timer_)) != ZX_OK) {
return status;
}
llcpp::fuchsia::input::report::InputDevice::ResultOf::GetReportsEvent result =
keyboard_client_->GetReportsEvent();
if (result.status() != ZX_OK) {
return status;
}
keyboard_event_ = std::move(result->event);
input_notifier_.handle = keyboard_event_.get();
input_notifier_.waitfor = DEV_STATE_READABLE;
input_notifier_.func = [](port_handler* input_notifier, zx_signals_t signals, uint32_t evt) {
Keyboard* kbd = containerof(input_notifier, Keyboard, input_notifier_);
zx_status_t status = kbd->InputCallback(signals, evt);
if (status == ZX_ERR_STOP) {
delete kbd;
}
return status;
};
if ((status = port_wait(&port, &input_notifier_)) != ZX_OK) {
return status;
}
timer_notifier_.handle = timer_.get();
timer_notifier_.waitfor = ZX_TIMER_SIGNALED;
timer_notifier_.func = [](port_handler_t* ph, zx_signals_t signals, uint32_t evt) {
Keyboard* kbd = containerof(ph, Keyboard, timer_notifier_);
return kbd->TimerCallback(signals, evt);
};
port_wait(&port, &timer_notifier_);
return ZX_OK;
}
zx_status_t KeyboardWatcher::OpenFile(uint8_t evt, char* name) {
if ((evt != fuchsia_io_WATCH_EVENT_EXISTING) && (evt != fuchsia_io_WATCH_EVENT_ADDED)) {
return ZX_OK;
}
int fd;
if ((fd = openat(Fd(), name, O_RDONLY)) < 0) {
return ZX_OK;
}
zx::channel chan;
zx_status_t status = fdio_get_service_handle(fd, chan.reset_and_get_address());
if (status != ZX_OK) {
return status;
}
auto keyboard_client = llcpp::fuchsia::input::report::InputDevice::SyncClient(std::move(chan));
llcpp::fuchsia::input::report::InputDevice::ResultOf::GetDescriptor result =
keyboard_client.GetDescriptor();
if (result.status() != ZX_OK) {
return result.status();
}
// Skip devices that aren't keyboards.
if (!result->descriptor.has_keyboard()) {
return ZX_ERR_NOT_SUPPORTED;
}
printf("vc: new input device /dev/class/input-report/%s\n", name);
// This is not a memory leak, because keyboards free themselves when their underlying
// devices close.
Keyboard* keyboard = new Keyboard(handler_, repeat_keys_);
status = keyboard->Setup(std::move(keyboard_client));
if (status != ZX_OK) {
delete keyboard;
return status;
}
return ZX_OK;
}
zx_status_t KeyboardWatcher::DirCallback(port_handler_t* ph, zx_signals_t signals, uint32_t evt) {
if (!(signals & ZX_CHANNEL_READABLE)) {
printf("vc: device directory died\n");
return ZX_ERR_STOP;
}
// Buffer contains events { Opcode, Len, Name[Len] }
// See zircon/device/vfs.h for more detail
// extra byte is for temporary NUL
std::array<uint8_t, fuchsia_io_MAX_BUF + 1> buffer;
uint32_t len;
if (zx_channel_read(ph->handle, 0, buffer.data(), NULL, buffer.size() - 1, 0, &len, NULL) < 0) {
printf("vc: failed to read from device directory\n");
return ZX_ERR_STOP;
}
uint32_t index = 0;
while (index + 2 <= len) {
uint8_t event = buffer[index];
uint8_t namelen = buffer[index + 1];
index += 2;
if ((namelen + index) > len) {
printf("vc: malformed device directory message\n");
return ZX_ERR_STOP;
}
// add temporary nul
uint8_t tmp = buffer[index + namelen];
buffer[index + namelen] = 0;
OpenFile(event, reinterpret_cast<char*>(&buffer[index]));
buffer[index + namelen] = tmp;
index += namelen;
}
return ZX_OK;
}
zx_status_t KeyboardWatcher::Setup(keypress_handler_t handler, bool repeat_keys) {
repeat_keys_ = repeat_keys;
handler_ = handler;
fbl::unique_fd fd(open("/dev/class/input-report", O_DIRECTORY | O_RDONLY));
if (!fd) {
return ZX_ERR_IO;
}
zx::channel client, server;
zx_status_t status;
if ((status = zx::channel::create(0, &client, &server)) != ZX_OK) {
return status;
}
dir_caller_ = fdio_cpp::FdioCaller(std::move(fd));
zx_status_t io_status = fuchsia_io_DirectoryWatch(
dir_caller_.borrow_channel(), fuchsia_io_WATCH_MASK_ALL, 0, server.release(), &status);
if (io_status != ZX_OK || status != ZX_OK) {
return io_status;
}
dir_handler_.handle = client.release();
dir_handler_.waitfor = ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED;
dir_handler_.func = keyboard_main_callback;
port_wait(&port, &dir_handler_);
return ZX_OK;
}