| // Copyright 2021 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "minios/key_reader.h" | |
| #include <ctype.h> | |
| #include <dirent.h> | |
| #include <fcntl.h> | |
| #include <linux/input.h> | |
| #include <sys/epoll.h> | |
| #include <unistd.h> | |
| #include <algorithm> | |
| #include <utility> | |
| #include <vector> | |
| #include <base/files/file_enumerator.h> | |
| #include <base/logging.h> | |
| #include <base/strings/strcat.h> | |
| #include <base/values.h> | |
| namespace minios { | |
| // Key state parameter. | |
| const int kFdsMax = 10; | |
| namespace { | |
| constexpr char kDevInputEvent[] = "/dev/input"; | |
| constexpr char kEventDevName[] = "*event*"; | |
| constexpr char kXkbPathName[] = "/usr/share/X11/xkb"; | |
| // Offset between xkb layout codes and ev key codes. | |
| constexpr int kXkbOffset = 8; | |
| // Determines if the given |bit| is set in the |bitmask| array. | |
| bool TestBit(const int bit, const uint8_t* bitmask) { | |
| return (bitmask[bit / 8] >> (bit % 8)) & 1; | |
| } | |
| bool IsUsbDevice(const int fd) { | |
| struct input_id id; | |
| if (ioctl(fd, EVIOCGID, &id) == -1) { | |
| PLOG(ERROR) << "Failed to ioctl to determine device bus"; | |
| return false; | |
| } | |
| return id.bustype == BUS_USB; | |
| } | |
| bool IsKeyboardDevice(const int fd) { | |
| uint8_t evtype_bitmask[EV_MAX / 8 + 1]; | |
| if (ioctl(fd, EVIOCGBIT(0, sizeof(evtype_bitmask)), evtype_bitmask) == -1) { | |
| PLOG(ERROR) << "Failed to ioctl to determine supported event types"; | |
| return false; | |
| } | |
| // The device is a "keyboard" if it supports EV_KEY events. Though, it is not | |
| // necessarily a real keyboard; EV_KEY events could also be e.g. volume | |
| // up/down buttons on a device. | |
| return TestBit(EV_KEY, evtype_bitmask); | |
| } | |
| } // namespace | |
| KeyReader::KeyReader(bool include_usb) | |
| : include_usb_(include_usb), | |
| use_only_evwaitkey_(true), | |
| delegate_(nullptr) {} | |
| KeyReader::KeyReader(bool include_usb, std::string keyboard_layout) | |
| : repeated_key_(0), | |
| repeated_counter_(0), | |
| return_pressed_(false), | |
| include_usb_(include_usb), | |
| keyboard_layout_(keyboard_layout), | |
| use_only_evwaitkey_(false) { | |
| user_input_.reserve(kMaxInputLength); | |
| } | |
| KeyReader::~KeyReader() { | |
| // Release xkb references. | |
| if (ctx_ != nullptr) { | |
| xkb_state_unref(state_); | |
| xkb_keymap_unref(keymap_); | |
| xkb_context_unref(ctx_); | |
| } | |
| } | |
| bool KeyReader::Init(const std::vector<int>& valid_keys) { | |
| keys_ = valid_keys; | |
| if (!GetValidFds(/*check_supported_keys=*/true)) { | |
| LOG(ERROR) << "No valid input devices found."; | |
| return false; | |
| } | |
| if (!EpollCreate(&epfd_)) { | |
| PLOG(ERROR) << " EpollCreate failed, cannot watch epfd."; | |
| return false; | |
| } | |
| // Set the file descriptor watcher. | |
| return StartWatcher(); | |
| } | |
| bool KeyReader::SupportsAllKeys(const int fd) { | |
| uint8_t key_bitmask[KEY_MAX / 8 + 1]; | |
| if (ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(key_bitmask)), key_bitmask) == -1) { | |
| PLOG(ERROR) << "Failed to ioctl to determine supported key events"; | |
| return false; | |
| } | |
| for (const auto& key : keys_) { | |
| if (!TestBit(key, key_bitmask)) | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool KeyReader::GetValidFds(bool check_supported_keys) { | |
| fds_.clear(); | |
| base::FileEnumerator file_enumerator(base::FilePath(kDevInputEvent), true, | |
| base::FileEnumerator::FILES, | |
| FILE_PATH_LITERAL(kEventDevName)); | |
| for (base::FilePath dir_path = file_enumerator.Next(); !dir_path.empty(); | |
| dir_path = file_enumerator.Next()) { | |
| base::ScopedFD fd(open(dir_path.value().c_str(), O_RDONLY | O_CLOEXEC)); | |
| if (!fd.is_valid()) { | |
| continue; | |
| } | |
| if ((include_usb_ || !IsUsbDevice(fd.get())) && | |
| IsKeyboardDevice(fd.get())) { | |
| if (!check_supported_keys || SupportsAllKeys(fd.get())) { | |
| fds_.push_back(std::move(fd)); | |
| } | |
| } | |
| } | |
| return !fds_.empty(); | |
| } | |
| bool KeyReader::EpollCreate(base::ScopedFD* epfd) { | |
| *epfd = base::ScopedFD(epoll_create1(EPOLL_CLOEXEC)); | |
| if (epfd->get() < 0) { | |
| PLOG(ERROR) << "Epoll_create failed"; | |
| return false; | |
| } | |
| for (int i = 0; i < fds_.size(); ++i) { | |
| struct epoll_event ep_event { | |
| .events = EPOLLIN, .data.u32 = static_cast<uint32_t>(i), | |
| }; | |
| if (epoll_ctl(epfd->get(), EPOLL_CTL_ADD, fds_[i].get(), &ep_event) < 0) { | |
| PLOG(ERROR) << "Epoll_ctl failed"; | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| bool KeyReader::GetEpEvent(int epfd, struct input_event* ev) { | |
| struct epoll_event ep_event; | |
| if (epoll_wait(epfd, &ep_event, 1, -1) <= 0) { | |
| PLOG(ERROR) << "epoll_wait failed"; | |
| return false; | |
| } | |
| if (HANDLE_EINTR(read(fds_[ep_event.data.u32].get(), ev, sizeof(*ev))) != | |
| sizeof(*ev)) { | |
| PLOG(ERROR) << "Could not read event"; | |
| return false; | |
| } | |
| return true; | |
| } | |
| void KeyReader::OnKeyEvent() { | |
| struct input_event ev; | |
| if (!GetEpEvent(epfd_.get(), &ev)) { | |
| PLOG(ERROR) << "Could not get event"; | |
| return; | |
| } | |
| if (!delegate_) { | |
| return; | |
| } | |
| if (ev.type != EV_KEY || ev.code > KEY_MAX) { | |
| return; | |
| } | |
| if (std::find(keys_.begin(), keys_.end(), ev.code) == keys_.end()) { | |
| return; | |
| } | |
| switch (ev.value) { | |
| case kKeyPress: | |
| delegate_->OnKeyPress(ev.code); | |
| repeated_counter_ = 0; | |
| return; | |
| case kKeyHold: | |
| if (repeated_key_ != ev.code) { | |
| repeated_key_ = ev.code; | |
| repeated_counter_ = 0; | |
| return; | |
| } | |
| if (++repeated_counter_ < kRepeatedSensitivity) { | |
| return; | |
| } | |
| repeated_counter_ = 0; | |
| delegate_->OnKeyPress(ev.code); | |
| return; | |
| default: | |
| return; | |
| } | |
| } | |
| bool KeyReader::SetKeyboardContext() { | |
| // Set xkb layout and get keymap. | |
| ctx_ = xkb_context_new(XKB_CONTEXT_NO_DEFAULT_INCLUDES); | |
| if (!ctx_) { | |
| LOG(ERROR) << "Unable to get new xkb context."; | |
| return false; | |
| } | |
| if (!xkb_context_include_path_append(ctx_, kXkbPathName)) { | |
| LOG(ERROR) << "Cannot add path " << kXkbPathName << " to context."; | |
| return false; | |
| } | |
| names_ = {.layout = keyboard_layout_.c_str()}; | |
| keymap_ = | |
| xkb_keymap_new_from_names(ctx_, &names_, XKB_KEYMAP_COMPILE_NO_FLAGS); | |
| if (keymap_ == nullptr) { | |
| LOG(ERROR) << "No matching keyboard for " << keyboard_layout_ | |
| << ". Make sure the two letter country code is valid."; | |
| return false; | |
| } | |
| state_ = xkb_state_new(keymap_); | |
| if (!state_) { | |
| LOG(ERROR) << "Unable to get xkbstate for " << keyboard_layout_; | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool KeyReader::InputSetUp() { | |
| if (use_only_evwaitkey_) { | |
| LOG(ERROR) << "Please construct the class with include_usb and " | |
| "country_code in order to correctly use this function."; | |
| return false; | |
| } | |
| if (!GetValidFds(/*check_supported_keys=*/false)) { | |
| LOG(ERROR) << "No valid input devices found."; | |
| return false; | |
| } | |
| if (!EpollCreate(&epfd_)) { | |
| return false; | |
| } | |
| if (!SetKeyboardContext()) { | |
| return false; | |
| } | |
| user_input_.clear(); | |
| return true; | |
| } | |
| bool KeyReader::GetChar(const struct input_event& ev, bool* tab_toggle) { | |
| const xkb_keycode_t key_code = ev.code + kXkbOffset; | |
| if (ev.value == kKeyRelease) { | |
| // Update underlying state on release. | |
| xkb_state_update_key(state_, key_code, XKB_KEY_UP); | |
| } else if (ev.value == kKeyPress) { | |
| xkb_state_update_key(state_, key_code, XKB_KEY_DOWN); | |
| const xkb_keysym_t sym = xkb_state_key_get_one_sym(state_, key_code); | |
| // Only return false when `return` key is pressed. | |
| if (sym == XKB_KEY_Return) | |
| return false; | |
| if (sym == XKB_KEY_Tab) { | |
| *tab_toggle = !(*tab_toggle); | |
| return true; | |
| } | |
| // Put char representation in buffer. | |
| int size = xkb_state_key_get_utf8(state_, key_code, nullptr, 0) + 1; | |
| std::vector<char> buff(size); | |
| xkb_state_key_get_utf8(state_, key_code, buff.data(), size); | |
| if (sym == XKB_KEY_BackSpace && !user_input_.empty()) { | |
| user_input_.pop_back(); | |
| } else if (isprint(buff[0]) && user_input_.size() < kMaxInputLength) { | |
| // Only printable ASCII characters stored in output. | |
| user_input_.push_back(buff[0]); | |
| } | |
| } else if (ev.value == kKeyHold) { | |
| // Long press or repeating key event. Only backspace is supported in this | |
| // case, ignore all other keys. | |
| const xkb_keysym_t sym = xkb_state_key_get_one_sym(state_, key_code); | |
| if (repeated_key_ != ev.code) { | |
| repeated_key_ = ev.code; | |
| repeated_counter_ = 0; | |
| } | |
| if (sym == XKB_KEY_BackSpace && !user_input_.empty() && | |
| ++repeated_counter_ >= kRepeatedSensitivity) { | |
| // Remove characters until empty. | |
| user_input_.pop_back(); | |
| repeated_counter_ = 0; | |
| } | |
| } | |
| return true; | |
| } | |
| bool KeyReader::GetUserInput(bool* enter, | |
| bool* tab_toggle, | |
| std::string* user_input) { | |
| struct input_event ev; | |
| if (!GetEpEvent(epfd_.get(), &ev)) { | |
| PLOG(ERROR) << "Could not get event"; | |
| return false; | |
| } | |
| if (ev.type != EV_KEY || ev.code > KEY_MAX) { | |
| return true; | |
| } | |
| // Take in ev event and add to user input as appropriate. | |
| // Returns false to indicate enter was pressed. | |
| if (!GetChar(ev, tab_toggle)) { | |
| *enter = true; | |
| } | |
| *user_input = user_input_; | |
| return true; | |
| } | |
| bool KeyReader::StartWatcher() { | |
| watcher_ = base::FileDescriptorWatcher::WatchReadable( | |
| epfd_.get(), | |
| base::BindRepeating(&KeyReader::OnKeyEvent, base::Unretained(this))); | |
| if (!watcher_) { | |
| LOG(ERROR) << "Failed to watch epoll fd."; | |
| return false; | |
| } | |
| return true; | |
| } | |
| void KeyReader::StopWatcher() { | |
| watcher_ = nullptr; | |
| } | |
| bool KeyReader::GetCharForTest(const struct input_event& ev) { | |
| bool tab_key = false; | |
| return GetChar(ev, &tab_key); | |
| } | |
| std::string KeyReader::GetUserInputForTest() { | |
| return user_input_; | |
| } | |
| } // namespace minios |