| // Copyright 2021 The Chromium OS 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 "minios/key_reader.h" |
| |
| #include <ctype.h> |
| #include <dirent.h> |
| #include <fcntl.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 parameters. |
| const int kFdsMax = 10; |
| const int kKeyMax = 200; |
| |
| 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, int* index) { |
| struct epoll_event ep_event; |
| if (epoll_wait(epfd, &ep_event, 1, -1) <= 0) { |
| PLOG(ERROR) << "epoll_wait failed"; |
| return false; |
| } |
| *index = ep_event.data.u32; |
| if (read(fds_[*index].get(), ev, sizeof(*ev)) != sizeof(*ev)) { |
| PLOG(ERROR) << "Could not read event"; |
| return false; |
| } |
| return true; |
| } |
| |
| void KeyReader::OnKeyEvent() { |
| struct input_event ev; |
| int index = 0; |
| if (!GetEpEvent(epfd_.get(), &ev, &index)) { |
| PLOG(ERROR) << "Could not get event"; |
| return; |
| } |
| if (ev.type != EV_KEY || ev.code > KEY_MAX) { |
| return; |
| } |
| |
| if (std::find(keys_.begin(), keys_.end(), ev.code) == keys_.end()) { |
| return; |
| } |
| |
| if (!delegate_) { |
| LOG(ERROR) << "Delegate not initialized."; |
| return; |
| } |
| |
| if (ev.value == 2) { |
| if (repeated_key_ != ev.code) { |
| repeated_key_ = ev.code; |
| repeated_counter_ = 0; |
| } |
| if (++repeated_counter_ < kRepeatedSensitivity) |
| return; |
| // Repeated key press and release. |
| delegate_->OnKeyPress(index, ev.code, false); |
| delegate_->OnKeyPress(index, ev.code, true); |
| } else { |
| delegate_->OnKeyPress(index, ev.code, (ev.value == 0)); |
| } |
| repeated_counter_ = 0; |
| } |
| |
| 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) { |
| xkb_keycode_t keycode = ev.code + kXkbOffset; |
| xkb_keysym_t sym = xkb_state_key_get_one_sym(state_, keycode); |
| if (ev.value == 0) { |
| // Key up event. |
| if (sym == XKB_KEY_Return && return_pressed_) { |
| // Only end if RETURN key press was already recorded. |
| return false; |
| } else if (sym == XKB_KEY_Tab) { |
| *tab_toggle = !(*tab_toggle); |
| } |
| |
| // Put char representation in buffer. |
| int size = xkb_state_key_get_utf8(state_, keycode, nullptr, 0) + 1; |
| std::vector<char> buff(size); |
| xkb_state_key_get_utf8(state_, keycode, 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]); |
| } |
| xkb_state_update_key(state_, keycode, XKB_KEY_UP); |
| } else if (ev.value == 1) { |
| // Key down event. |
| if (sym == XKB_KEY_Return) |
| return_pressed_ = true; |
| |
| xkb_state_update_key(state_, keycode, XKB_KEY_DOWN); |
| |
| } else if (ev.value == 2) { |
| // Long press or repeating key event. |
| 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; |
| int index = 0; |
| if (!GetEpEvent(epfd_.get(), &ev, &index)) { |
| 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 |