blob: 8e99189ceddd2a306ee93d880a9cebb2ead39fff [file] [log] [blame]
// 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/check.h>
#include <base/check_op.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/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 <base/threading/thread_task_runner_handle.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::ThreadTaskRunnerHandle::Get()->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);
std::shared_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