| // Copyright 2014 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 "power_manager/powerd/system/input_watcher.h" |
| |
| #include <linux/input.h> |
| #include <unistd.h> |
| |
| #include <base/callback.h> |
| #include <base/files/file_enumerator.h> |
| #include <base/files/file_path.h> |
| #include <base/files/file_util.h> |
| #include <base/logging.h> |
| #include <base/message_loop/message_loop.h> |
| #include <base/posix/eintr_wrapper.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <base/strings/string_split.h> |
| #include <base/strings/string_util.h> |
| #include <base/strings/stringprintf.h> |
| |
| #include "power_manager/common/power_constants.h" |
| #include "power_manager/common/prefs.h" |
| #include "power_manager/common/util.h" |
| #include "power_manager/powerd/system/acpi_wakeup_helper.h" |
| #include "power_manager/powerd/system/event_device_interface.h" |
| #include "power_manager/powerd/system/input_observer.h" |
| #include "power_manager/powerd/system/udev.h" |
| #include "power_manager/powerd/system/wakeup_device.h" |
| |
| namespace power_manager { |
| namespace system { |
| |
| namespace { |
| |
| const char kSysClassInputPath[] = "/sys/class/input"; |
| const char kDevInputPath[] = "/dev/input"; |
| const char kInputBaseName[] = "event"; |
| |
| const char kInputMatchPattern[] = "input*"; |
| const char kUsbMatchString[] = "usb"; |
| const char kBluetoothMatchString[] = "bluetooth"; |
| |
| // Given a string |name| consisting of kInputBaseName followed by a base-10 |
| // integer, extracts the integer to |num_out|. Returns false if |name| didn't |
| // match the expected format. |
| bool GetInputNumber(const std::string& name, int* num_out) { |
| if (!base::StartsWith(name, kInputBaseName, base::CompareCase::SENSITIVE)) |
| return false; |
| size_t base_len = strlen(kInputBaseName); |
| return base::StringToInt(name.substr(base_len, name.size() - base_len), |
| num_out); |
| } |
| |
| // If |event| came from a lid switch, copies its state to |state_out| and |
| // returns true. Otherwise, leaves |state_out| untouched and returns false. |
| bool GetLidStateFromEvent(const input_event& event, LidState* state_out) { |
| if (event.type != EV_SW || event.code != SW_LID) |
| return false; |
| |
| *state_out = event.value == 1 ? LidState::CLOSED : LidState::OPEN; |
| return true; |
| } |
| |
| // If |event| came from a tablet mode switch, copies its state to |mode_out| and |
| // returns true. Otherwise, leaves |mode_out| untouched and returns false. |
| bool GetTabletModeFromEvent(const input_event& event, TabletMode* mode_out) { |
| if (event.type != EV_SW || event.code != SW_TABLET_MODE) |
| return false; |
| |
| *mode_out = event.value == 1 ? TabletMode::ON : TabletMode::OFF; |
| return true; |
| } |
| |
| // If |event| came from a power button, copies its state to |state_out| and |
| // returns true. Otherwise, leaves |state_out| untouched and returns false. |
| bool GetPowerButtonStateFromEvent(const input_event& event, |
| ButtonState* state_out) { |
| if (event.type != EV_KEY || event.code != KEY_POWER) |
| return false; |
| |
| switch (event.value) { |
| case 0: |
| *state_out = ButtonState::UP; |
| break; |
| case 1: |
| *state_out = ButtonState::DOWN; |
| break; |
| case 2: |
| *state_out = ButtonState::REPEAT; |
| break; |
| default: |
| LOG(ERROR) << "Unhandled button state " << event.value; |
| return false; |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| const char InputWatcher::kPowerButtonToSkip[] = "LNXPWRBN"; |
| const char InputWatcher::kPowerButtonToSkipForLegacy[] = "isa"; |
| const char InputWatcher::kAcpiLidDevice[] = "PNP0C0D"; |
| |
| InputWatcher::InputWatcher() |
| : dev_input_path_(kDevInputPath), |
| sys_class_input_path_(kSysClassInputPath), |
| power_button_to_skip_(kPowerButtonToSkip), |
| acpi_lid_device_(kAcpiLidDevice), |
| weak_ptr_factory_(this) {} |
| |
| InputWatcher::~InputWatcher() { |
| if (udev_) |
| udev_->RemoveSubsystemObserver(kInputUdevSubsystem, this); |
| } |
| |
| bool InputWatcher::Init( |
| std::unique_ptr<EventDeviceFactoryInterface> event_device_factory, |
| PrefsInterface* prefs, |
| UdevInterface* udev) { |
| event_device_factory_ = std::move(event_device_factory); |
| udev_ = udev; |
| |
| prefs->GetBool(kUseLidPref, &use_lid_); |
| if (!use_lid_) |
| lid_state_ = LidState::NOT_PRESENT; |
| |
| bool legacy_power_button = false; |
| if (prefs->GetBool(kLegacyPowerButtonPref, &legacy_power_button) && |
| legacy_power_button) |
| power_button_to_skip_ = kPowerButtonToSkipForLegacy; |
| |
| prefs->GetBool(kDetectHoverPref, &detect_hover_); |
| |
| udev_->AddSubsystemObserver(kInputUdevSubsystem, this); |
| |
| std::vector<UdevDeviceInfo> input_device_list; |
| if (udev_->GetSubsystemDevices(kInputUdevSubsystem, &input_device_list)) { |
| for (auto const& input_device : input_device_list) { |
| int num = -1; |
| if (GetInputNumber(input_device.sysname, &num)) { |
| HandleAddedInput(input_device.sysname, num, false /* notify_state */); |
| } |
| } |
| } else { |
| LOG(ERROR) << "Enumeration of existing input devices failed. User " |
| "interaction might not be recognized properly"; |
| } |
| |
| return true; |
| } |
| |
| void InputWatcher::AddObserver(InputObserver* observer) { |
| DCHECK(observer); |
| observers_.AddObserver(observer); |
| } |
| |
| void InputWatcher::RemoveObserver(InputObserver* observer) { |
| DCHECK(observer); |
| observers_.RemoveObserver(observer); |
| } |
| |
| LidState InputWatcher::QueryLidState() { |
| if (!lid_device_) |
| return LidState::NOT_PRESENT; |
| |
| const uint32_t device_types = GetDeviceTypes(lid_device_); |
| while (true) { |
| // Stop when we fail to read any more events. |
| std::vector<input_event> events; |
| if (!lid_device_->ReadEvents(&events)) |
| break; |
| |
| // Get the state from the last lid event (|events| may also contain non-lid |
| // events). |
| for (std::vector<input_event>::const_reverse_iterator it = events.rbegin(); |
| it != events.rend(); ++it) { |
| if (GetLidStateFromEvent(*it, &lid_state_)) |
| break; |
| } |
| |
| queued_events_.reserve(queued_events_.size() + events.size()); |
| for (auto event : events) |
| queued_events_.push_back(std::make_pair(event, device_types)); |
| VLOG(1) << "Queued " << events.size() |
| << " event(s) while querying lid state"; |
| } |
| |
| if (!queued_events_.empty()) { |
| send_queued_events_task_.Reset( |
| base::Bind(&InputWatcher::SendQueuedEvents, base::Unretained(this))); |
| base::MessageLoop::current()->task_runner()->PostTask( |
| FROM_HERE, send_queued_events_task_.callback()); |
| } |
| |
| return lid_state_; |
| } |
| |
| TabletMode InputWatcher::GetTabletMode() { |
| return tablet_mode_; |
| } |
| |
| bool InputWatcher::IsUSBInputDeviceConnected() const { |
| base::FileEnumerator enumerator( |
| sys_class_input_path_, false, |
| static_cast<::base::FileEnumerator::FileType>( |
| base::FileEnumerator::FILES | base::FileEnumerator::SHOW_SYM_LINKS), |
| kInputMatchPattern); |
| for (base::FilePath path = enumerator.Next(); !path.empty(); |
| path = enumerator.Next()) { |
| base::FilePath symlink_path; |
| if (!base::ReadSymbolicLink(path, &symlink_path)) |
| continue; |
| const std::string& path_string = symlink_path.value(); |
| // Skip bluetooth devices, which may be identified as USB devices. |
| if (path_string.find(kBluetoothMatchString) != std::string::npos) |
| continue; |
| // Now look for the USB devices that are not bluetooth. |
| size_t position = path_string.find(kUsbMatchString); |
| if (position == std::string::npos) |
| continue; |
| // Now that the string "usb" has been found, make sure it is a whole word |
| // and not just part of another word like "busbreaker". |
| bool usb_at_word_head = |
| position == 0 || !base::IsAsciiAlpha(path_string.at(position - 1)); |
| bool usb_at_word_tail = |
| position + strlen(kUsbMatchString) == path_string.size() || |
| !base::IsAsciiAlpha(path_string.at(position + strlen(kUsbMatchString))); |
| if (usb_at_word_head && usb_at_word_tail) |
| return true; |
| } |
| return false; |
| } |
| |
| void InputWatcher::OnUdevEvent(const UdevEvent& event) { |
| DCHECK_EQ(event.device_info.subsystem, kInputUdevSubsystem); |
| int input_num = -1; |
| if (GetInputNumber(event.device_info.sysname, &input_num)) { |
| if (event.action == UdevEvent::Action::ADD) { |
| HandleAddedInput(event.device_info.sysname, input_num, |
| true /* notify_state */); |
| } else if (event.action == UdevEvent::Action::REMOVE) { |
| HandleRemovedInput(input_num); |
| } |
| } |
| } |
| |
| uint32_t InputWatcher::GetDeviceTypes( |
| const EventDeviceInterface* device) const { |
| uint32_t device_types = DEVICE_NONE; |
| if (power_button_devices_.count(device)) |
| device_types |= DEVICE_POWER_BUTTON; |
| if (device == lid_device_) |
| device_types |= DEVICE_LID_SWITCH; |
| if (device == tablet_mode_device_) |
| device_types |= DEVICE_TABLET_MODE_SWITCH; |
| if (device == hover_device_) |
| device_types |= DEVICE_HOVER; |
| return device_types; |
| } |
| |
| void InputWatcher::OnNewEvents(EventDeviceInterface* device) { |
| SendQueuedEvents(); |
| |
| std::vector<input_event> events; |
| if (!device->ReadEvents(&events)) |
| return; |
| |
| VLOG(1) << "Read " << events.size() << " event(s) from " |
| << device->GetDebugName(); |
| const uint32_t device_types = GetDeviceTypes(device); |
| for (size_t i = 0; i < events.size(); ++i) { |
| // Update |lid_state_| here instead of in ProcessEvent() so we can avoid |
| // modifying it in response to queued events. |
| if (device_types & DEVICE_LID_SWITCH) |
| GetLidStateFromEvent(events[i], &lid_state_); |
| ProcessEvent(events[i], device_types); |
| } |
| } |
| |
| void InputWatcher::ProcessEvent(const input_event& event, |
| uint32_t device_types) { |
| LidState lid_state = LidState::OPEN; |
| if ((device_types & DEVICE_LID_SWITCH) && |
| GetLidStateFromEvent(event, &lid_state)) { |
| VLOG(1) << "Notifying observers about lid " << LidStateToString(lid_state) |
| << " event"; |
| for (InputObserver& observer : observers_) |
| observer.OnLidEvent(lid_state); |
| } |
| |
| TabletMode tablet_mode = TabletMode::OFF; |
| if (device_types & DEVICE_TABLET_MODE_SWITCH && |
| GetTabletModeFromEvent(event, &tablet_mode)) { |
| tablet_mode_ = tablet_mode; |
| VLOG(1) << "Notifying observers about tablet mode " |
| << TabletModeToString(tablet_mode) << " event"; |
| for (InputObserver& observer : observers_) |
| observer.OnTabletModeEvent(tablet_mode); |
| } |
| |
| ButtonState button_state = ButtonState::DOWN; |
| if ((device_types & DEVICE_POWER_BUTTON) && |
| GetPowerButtonStateFromEvent(event, &button_state)) { |
| VLOG(1) << "Notifying observers about power button " |
| << ButtonStateToString(button_state) << " event"; |
| for (InputObserver& observer : observers_) |
| observer.OnPowerButtonEvent(button_state); |
| } |
| |
| if (device_types & DEVICE_HOVER) |
| ProcessHoverEvent(event); |
| } |
| |
| void InputWatcher::ProcessHoverEvent(const input_event& event) { |
| if (event.type == EV_ABS && event.code == ABS_MT_SLOT) { |
| VLOG(2) << "ABS_MT_SLOT " << event.value; |
| // ABS_MT_SLOT events announce the slot that following multitouch events |
| // will refer to. |
| if (event.value < 0 || |
| event.value >= |
| static_cast<int>(sizeof(multitouch_slots_hover_state_) * 8)) { |
| LOG(WARNING) << "Ignoring ABS_MT_SLOT event for slot " << event.value; |
| current_multitouch_slot_ = -1; |
| } else { |
| current_multitouch_slot_ = event.value; |
| } |
| } else if (event.type == EV_ABS && event.code == ABS_MT_TRACKING_ID) { |
| VLOG(2) << "ABS_MT_TRACKING_ID " << event.value; |
| // ABS_MT_TRACKING_ID events associate a tracking ID with the current slot, |
| // with -1 indicating that the slot is unused. Use them as a proxy for |
| // whether the slot is reporting a hover (or touch). |
| if (current_multitouch_slot_ >= 0) { |
| const uint64_t slot_bit = static_cast<uint64_t>(1) |
| << current_multitouch_slot_; |
| if (event.value >= 0) |
| multitouch_slots_hover_state_ |= slot_bit; |
| else |
| multitouch_slots_hover_state_ &= ~slot_bit; |
| } |
| } else if (event.type == EV_ABS && event.code == ABS_DISTANCE) { |
| // For single-touch presence-only hover touchpads, ABS_DISTANCE indicates |
| // the distance above the pad the single-touch finger is hovering |
| VLOG(2) << "ABS_DISTANCE " << event.value; |
| single_touch_hover_distance_nonzero_ = (event.value > 0); |
| } else if (event.type == EV_KEY && event.code == BTN_TOOL_FINGER) { |
| // For single-touch presence-only hover touchpads, BTN_TOOL_FINGER tells |
| // us if the single-touch contact is valid (if we should believe the |
| // value in ABS_DISTANCE) |
| VLOG(2) << "BTN_TOOL_FINGER " << event.value; |
| single_touch_hover_valid_ = (event.value == 1); |
| } else if (event.type == EV_SYN && event.code == SYN_REPORT) { |
| // SYN_REPORT events indicate the end of the current set of multitouch data. |
| // Check whether the overall hovering state is different from before and |
| // notify observers if so. |
| VLOG(2) << "SYN_REPORT"; |
| bool multi_touch_hovering = multitouch_slots_hover_state_ != 0; |
| bool single_touch_hovering = |
| (single_touch_hover_distance_nonzero_ && single_touch_hover_valid_); |
| bool hovering = multi_touch_hovering || single_touch_hovering; |
| if (hovering != hovering_) { |
| VLOG(1) << "Notifying observers about hover state change to " |
| << (hovering ? "on" : "off"); |
| hovering_ = hovering; |
| for (InputObserver& observer : observers_) |
| observer.OnHoverStateChange(hovering_); |
| } |
| } |
| } |
| |
| void InputWatcher::HandleAddedInput(const std::string& input_name, |
| int input_num, |
| bool notify_state) { |
| if (event_devices_.count(input_num) > 0) { |
| LOG(WARNING) << "Input " << input_num << " already registered"; |
| return; |
| } |
| |
| const base::FilePath path = dev_input_path_.Append(input_name); |
| linked_ptr<EventDeviceInterface> device(event_device_factory_->Open(path)); |
| if (!device.get()) { |
| LOG(ERROR) << "Failed to open " << path.value(); |
| return; |
| } |
| |
| bool should_watch = false; |
| |
| const std::string phys = device->GetPhysPath(); |
| if (base::StartsWith(phys, power_button_to_skip_, |
| base::CompareCase::SENSITIVE)) { |
| VLOG(1) << "Skipping event device with phys path: " << phys; |
| } else if (device->IsPowerButton()) { |
| LOG(INFO) << "Watching power button: " << device->GetDebugName(); |
| should_watch = true; |
| power_button_devices_.insert(device.get()); |
| } |
| |
| // Note that it's possible for a power button and lid switch to share a single |
| // event device. |
| if (use_lid_ && device->IsLidSwitch()) { |
| if (lid_device_) { |
| LOG(WARNING) << "Skipping additional lid switch device " |
| << device->GetDebugName(); |
| } else { |
| LOG(INFO) << "Watching lid switch: " << device->GetDebugName(); |
| should_watch = true; |
| lid_device_ = device.get(); |
| lid_state_ = device->GetInitialLidState(); |
| VLOG(1) << "Initial lid state is " << LidStateToString(lid_state_); |
| if (notify_state) { |
| for (InputObserver& observer : observers_) |
| observer.OnLidEvent(lid_state_); |
| } |
| } |
| } |
| |
| if (device->IsTabletModeSwitch()) { |
| if (tablet_mode_device_) { |
| LOG(WARNING) << "Skipping additional tablet mode switch " |
| << device->GetDebugName(); |
| } else { |
| LOG(INFO) << "Watching tablet mode switch: " << device->GetDebugName(); |
| should_watch = true; |
| tablet_mode_device_ = device.get(); |
| tablet_mode_ = device->GetInitialTabletMode(); |
| VLOG(1) << "Initial tablet mode state is " |
| << TabletModeToString(tablet_mode_); |
| if (notify_state) { |
| for (InputObserver& observer : observers_) |
| observer.OnTabletModeEvent(tablet_mode_); |
| } |
| } |
| } |
| |
| if (detect_hover_ && device->HoverSupported() && device->HasLeftButton()) { |
| if (hover_device_) { |
| LOG(WARNING) << "Skipping additional hover device " |
| << device->GetDebugName(); |
| } else { |
| LOG(INFO) << "Watching hover device: " << device->GetDebugName(); |
| should_watch = true; |
| hover_device_ = device.get(); |
| } |
| } |
| |
| if (device->IsCrosFp()) { |
| should_watch = true; |
| LOG(INFO) << "Watching fingerprint device: " << device->GetDebugName(); |
| } |
| |
| if (should_watch) { |
| device->WatchForEvents(base::Bind(&InputWatcher::OnNewEvents, |
| weak_ptr_factory_.GetWeakPtr(), |
| base::Unretained(device.get()))); |
| event_devices_.insert(std::make_pair(input_num, device)); |
| } else { |
| VLOG(1) << "Event device with phys path " << device->GetDebugName() |
| << " is not monitored for input events"; |
| } |
| } |
| |
| void InputWatcher::HandleRemovedInput(int input_num) { |
| InputMap::iterator it = event_devices_.find(input_num); |
| if (it != event_devices_.end()) { |
| LOG(INFO) << "Stopping watching " << it->second->GetDebugName(); |
| power_button_devices_.erase(it->second.get()); |
| if (lid_device_ == it->second.get()) |
| lid_device_ = nullptr; |
| if (tablet_mode_device_ == it->second.get()) |
| tablet_mode_device_ = nullptr; |
| if (hover_device_ == it->second.get()) |
| hover_device_ = nullptr; |
| event_devices_.erase(it); |
| } |
| } |
| |
| void InputWatcher::SendQueuedEvents() { |
| for (auto event_pair : queued_events_) |
| ProcessEvent(event_pair.first, event_pair.second); |
| queued_events_.clear(); |
| } |
| |
| } // namespace system |
| } // namespace power_manager |