// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/webui/diagnostics_ui/backend/input/input_data_provider.h"
#include <fcntl.h>
#include <linux/input.h>
#include <vector>
#include "ash/accelerators/accelerator_controller_impl.h"
#include "ash/constants/ash_features.h"
#include "ash/events/event_rewriter_controller_impl.h"
#include "ash/public/cpp/tablet_mode.h"
#include "ash/shell.h"
#include "ash/system/diagnostics/diagnostics_log_controller.h"
#include "ash/system/diagnostics/keyboard_input_log.h"
#include "ash/system/diagnostics/mojom/input.mojom.h"
#include "ash/system/input_device_settings/input_device_settings_utils.h"
#include "ash/webui/diagnostics_ui/backend/common/histogram_util.h"
#include "ash/webui/diagnostics_ui/backend/input/event_watcher_factory.h"
#include "ash/webui/diagnostics_ui/backend/input/input_data_event_watcher.h"
#include "ash/webui/diagnostics_ui/backend/input/input_device_information.h"
#include "ash/webui/diagnostics_ui/backend/input/keyboard_input_data_event_watcher.h"
#include "ash/webui/diagnostics_ui/mojom/input_data_provider.mojom.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/window_util.h"
#include "base/logging.h"
#include "base/ranges/algorithm.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/display/screen.h"
#include "ui/events/ash/event_rewriter_ash.h"
#include "ui/events/devices/device_data_manager.h"
#include "ui/events/devices/input_device.h"
namespace ash {
namespace diagnostics {
namespace {
bool GetEventNodeId(base::FilePath path, int* id) {
const std::string base_name_prefix = "event";
std::string base_name = path.BaseName().value();
if (!base::StartsWith(base_name, base_name_prefix))
return false;
base_name.erase(0, base_name_prefix.length());
return base::StringToInt(base_name, id);
}
// Determine if this particular evdev provides touchpad or touchscreen input;
// we do not want stylus devices, which also claim to be touchscreens.
bool IsTouchInputDevice(InputDeviceInformation* device_info) {
return (device_info->event_device_info.HasTouchpad() ||
(device_info->event_device_info.HasTouchscreen() &&
!device_info->event_device_info.HasStylus()));
}
bool IsLoggingEnabled() {
return diagnostics::DiagnosticsLogController::IsInitialized();
}
} // namespace
// Escape should be able to close the dialog as long as shortcuts are not
// blocked. This boolean is updated within |BlockShortcuts|.
bool InputDataProvider::should_close_dialog_on_escape_ = true;
InputDataProvider::InputDataProvider(aura::Window* window)
: device_manager_(ui::CreateDeviceManager()),
watcher_factory_(std::make_unique<EventWatcherFactoryImpl>()),
accelerator_controller_(Shell::Get()->accelerator_controller()),
event_rewriter_delegate_(Shell::Get()
->event_rewriter_controller()
->event_rewriter_ash_delegate()) {
Initialize(window);
}
InputDataProvider::InputDataProvider(
aura::Window* window,
std::unique_ptr<ui::DeviceManager> device_manager_for_test,
std::unique_ptr<EventWatcherFactory> watcher_factory,
AcceleratorControllerImpl* accelerator_controller,
ui::EventRewriterAsh::Delegate* event_rewriter_delegate)
: device_manager_(std::move(device_manager_for_test)),
watcher_factory_(std::move(watcher_factory)),
accelerator_controller_(accelerator_controller),
event_rewriter_delegate_(event_rewriter_delegate) {
Initialize(window);
}
InputDataProvider::~InputDataProvider() {
// Cleanup all the keyboard watchers/observers.
for (const auto& [id, _] : keyboard_watchers_) {
UnforwardKeyboardInput(id);
}
BlockShortcuts(/*should_block=*/false);
device_manager_->RemoveObserver(this);
widget_->RemoveObserver(this);
TabletMode::Get()->RemoveObserver(this);
chromeos::PowerManagerClient::Get()->RemoveObserver(this);
ash::Shell::Get()->display_configurator()->RemoveObserver(this);
}
// static
mojom::ConnectionType InputDataProvider::ConnectionTypeFromInputDeviceType(
ui::InputDeviceType type) {
switch (type) {
case ui::InputDeviceType::INPUT_DEVICE_INTERNAL:
return mojom::ConnectionType::kInternal;
case ui::InputDeviceType::INPUT_DEVICE_USB:
return mojom::ConnectionType::kUsb;
case ui::InputDeviceType::INPUT_DEVICE_BLUETOOTH:
return mojom::ConnectionType::kBluetooth;
case ui::InputDeviceType::INPUT_DEVICE_UNKNOWN:
return mojom::ConnectionType::kUnknown;
}
}
void InputDataProvider::Initialize(aura::Window* window) {
DCHECK(accelerator_controller_);
DCHECK(event_rewriter_delegate_);
// Window and widget are needed for security enforcement.
CHECK(window);
widget_ = views::Widget::GetWidgetForNativeWindow(window);
CHECK(widget_);
device_manager_->AddObserver(this);
device_manager_->ScanDevices(this);
widget_->AddObserver(this);
TabletMode::Get()->AddObserver(this);
ash::Shell::Get()->display_configurator()->AddObserver(this);
chromeos::PowerManagerClient* power_manager_client =
chromeos::PowerManagerClient::Get();
DCHECK(power_manager_client);
power_manager_client->AddObserver(this);
power_manager_client->GetSwitchStates(base::BindOnce(
&InputDataProvider::OnReceiveSwitchStates, weak_factory_.GetWeakPtr()));
UpdateMaySendEvents();
}
void InputDataProvider::BindInterface(
mojo::PendingReceiver<mojom::InputDataProvider> pending_receiver) {
receiver_.reset();
receiver_.Bind(std::move(pending_receiver));
}
void InputDataProvider::GetConnectedDevices(
GetConnectedDevicesCallback callback) {
bool has_internal_keyboard = false;
for (const ui::KeyboardDevice& keyboard :
ui::DeviceDataManager::GetInstance()->GetKeyboardDevices()) {
if (keyboard.type == ui::InputDeviceType::INPUT_DEVICE_INTERNAL &&
!IsSplitModifierKeyboard(keyboard.id)) {
has_internal_keyboard = true;
break;
}
}
// If there is an internal keyboard and keyboards_ size is zero (meaning the
// app hasn't added it yet but will), do not execute the callback, instead,
// save it to an internal variable and execute until the internal keyboard has
// been added.
if (has_internal_keyboard && keyboards_.empty()) {
get_connected_devices_callback_ =
base::BindOnce(&InputDataProvider::GetConnectedDevicesHelper,
weak_factory_.GetWeakPtr(), std::move(callback));
return;
}
GetConnectedDevicesHelper(std::move(callback));
}
void InputDataProvider::GetConnectedDevicesHelper(
GetConnectedDevicesCallback callback) {
std::vector<mojom::KeyboardInfoPtr> keyboard_vector;
keyboard_vector.reserve(keyboards_.size());
for (auto& keyboard_info : keyboards_) {
keyboard_vector.push_back(keyboard_info.second.Clone());
}
std::vector<mojom::TouchDeviceInfoPtr> touch_device_vector;
touch_device_vector.reserve(touch_devices_.size());
for (auto& touch_device_info : touch_devices_) {
touch_device_vector.push_back(touch_device_info.second.Clone());
}
base::ranges::sort(keyboard_vector, std::less<>(), &mojom::KeyboardInfo::id);
base::ranges::sort(touch_device_vector, std::less<>(),
&mojom::TouchDeviceInfo::id);
std::move(callback).Run(std::move(keyboard_vector),
std::move(touch_device_vector));
}
void InputDataProvider::ObserveConnectedDevices(
mojo::PendingRemote<mojom::ConnectedDevicesObserver> observer) {
connected_devices_observers_.Add(std::move(observer));
}
void InputDataProvider::OnWidgetVisibilityChanged(views::Widget* widget,
bool visible) {
UpdateEventObservers();
}
void InputDataProvider::OnWidgetActivationChanged(views::Widget* widget,
bool active) {
UpdateEventObservers();
}
void InputDataProvider::ObserveTabletMode(
mojo::PendingRemote<mojom::TabletModeObserver> observer,
ObserveTabletModeCallback callback) {
const auto* tablet_mode_controller = Shell::Get()->tablet_mode_controller();
DCHECK(tablet_mode_controller);
tablet_mode_observers_.Add(std::move(observer));
std::move(callback).Run(
tablet_mode_controller->AreInternalInputDeviceEventsBlocked());
}
void InputDataProvider::OnTabletModeEventsBlockingChanged() {
// For input diagnostics purposes, tablet mode only matters if internal input
// device events are being blocked. Thus, |is_tablet_mode| tracks whether
// input devices are blocked vs tablet mode directly.
const auto* tablet_mode_controller = Shell::Get()->tablet_mode_controller();
DCHECK(tablet_mode_controller);
const bool is_tablet_mode =
tablet_mode_controller->AreInternalInputDeviceEventsBlocked();
for (auto& observer : tablet_mode_observers_) {
observer->OnTabletModeChanged(is_tablet_mode);
}
}
void InputDataProvider::ObserveLidState(
mojo::PendingRemote<mojom::LidStateObserver> observer,
ObserveLidStateCallback callback) {
lid_state_observers_.Add(std::move(observer));
std::move(callback).Run(is_lid_open_);
}
void InputDataProvider::LidEventReceived(
chromeos::PowerManagerClient::LidState state,
base::TimeTicks time) {
// If the lid state is open or if the lid state sensors is not present, the
// lid is considered open
is_lid_open_ = state != chromeos::PowerManagerClient::LidState::CLOSED;
for (auto& observer : lid_state_observers_) {
observer->OnLidStateChanged(is_lid_open_);
}
}
void InputDataProvider::OnReceiveSwitchStates(
std::optional<chromeos::PowerManagerClient::SwitchStates> switch_states) {
if (switch_states.has_value()) {
LidEventReceived(switch_states->lid_state, /*time=*/{});
}
}
void InputDataProvider::ObserveInternalDisplayPowerState(
mojo::PendingRemote<mojom::InternalDisplayPowerStateObserver> observer) {
auto power_state =
Shell::Get()->display_configurator()->current_power_state();
is_internal_display_on_ =
power_state != chromeos::DISPLAY_POWER_INTERNAL_OFF_EXTERNAL_ON;
internal_display_power_state_observer_ =
mojo::Remote<mojom::InternalDisplayPowerStateObserver>(
std::move(observer));
}
void InputDataProvider::OnPowerStateChanged(
chromeos::DisplayPowerState power_state) {
if (internal_display_power_state_observer_.is_bound()) {
// Only when the internal display is off and external is on, we grey out
// the internal touchscreen test button.
is_internal_display_on_ =
power_state != chromeos::DISPLAY_POWER_INTERNAL_OFF_EXTERNAL_ON;
internal_display_power_state_observer_->OnInternalDisplayPowerStateChanged(
is_internal_display_on_);
}
}
void InputDataProvider::MoveAppToTestingScreen(uint32_t evdev_id) {
aura::Window* window = widget_->GetNativeWindow();
const int64_t current_display_id =
display::Screen::GetScreen()->GetDisplayNearestWindow(window).id();
// Find the testing touchscreen device.
auto it = touch_devices_.find((int)evdev_id);
if (it == touch_devices_.end())
return;
// Use device name to find the targeting display id.
// Since we use evdev_id as the device id in our implementation, which
// does not match the device id from ui::DeviceDataManager. So we use
// the name property to find the correct device. TODO(zhangwenyu): Double
// check if each touchscreen device from DeviceDataManager has a unique
// name.
for (const ui::TouchscreenDevice& device :
ui::DeviceDataManager::GetInstance()->GetTouchscreenDevices()) {
if (device.name == it->second->name) {
// Only move if the app is not already in the correct display.
if (current_display_id != device.target_display_id &&
device.target_display_id != display::kInvalidDisplayId &&
window_util::MoveWindowToDisplay(window, device.target_display_id)) {
// Only if window is successfully moved, we record the
// `previous_display_id_` so that we can move the window back when the
// tester is closed.
previous_display_id_ = current_display_id;
}
// Early break the loop as we've found the matching device, no matter if
// we have called the move function or not. e.g. if the device is
// already in the correct display.
break;
}
}
}
void InputDataProvider::MoveAppBackToPreviousScreen() {
if (previous_display_id_ != display::kInvalidDisplayId) {
window_util::MoveWindowToDisplay(widget_->GetNativeWindow(),
previous_display_id_);
}
// Always reset previous_display_id_ after MoveAppBackToPreviousScreen is
// called. So it won't affect next time the function is used.
previous_display_id_ = display::kInvalidDisplayId;
}
void InputDataProvider::SetA11yTouchPassthrough(bool enabled) {
widget_->GetNativeWindow()->SetProperty(
aura::client::kAccessibilityTouchExplorationPassThrough, enabled);
}
void InputDataProvider::UpdateMaySendEvents() {
const bool widget_open = !widget_->IsClosed();
const bool widget_active = widget_->IsActive();
const bool widget_visible = widget_->IsVisible();
may_send_events_ = widget_open && widget_visible && widget_active;
}
void InputDataProvider::UpdateEventObservers() {
const bool previous = may_send_events_;
UpdateMaySendEvents();
if (previous != may_send_events_) {
// If there are no observers, then we never want to block shortcuts.
// If there are observers, then we want to block when we are going to send
// events.
if (!keyboard_observers_.empty()) {
BlockShortcuts(may_send_events_);
}
if (!may_send_events_)
SendPauseEvents();
else
SendResumeEvents();
}
}
void InputDataProvider::BlockShortcuts(bool should_block) {
DCHECK(accelerator_controller_);
accelerator_controller_->SetPreventProcessingAccelerators(should_block);
DCHECK(event_rewriter_delegate_);
event_rewriter_delegate_->SuppressModifierKeyRewrites(should_block);
// While we are blocking shortcuts, esc should not close the diagnostcs
// dialog.
should_close_dialog_on_escape_ = !should_block;
}
void InputDataProvider::ForwardKeyboardInput(uint32_t id) {
if (!keyboards_.contains(id)) {
LOG(ERROR) << "Couldn't find keyboard with ID " << id
<< " when trying to forward input.";
return;
}
// If we are going to send keyboard events, we need to block shortcuts
BlockShortcuts(may_send_events_);
keyboard_watchers_[id] = watcher_factory_->MakeKeyboardEventWatcher(
id, weak_factory_.GetWeakPtr());
keyboard_tester_start_timestamp_ = base::Time::Now();
}
void InputDataProvider::UnforwardKeyboardInput(uint32_t id) {
if (!keyboards_.contains(id)) {
LOG(ERROR) << "Couldn't find keyboard with ID " << id
<< " when trying to unforward input.";
}
if (!keyboard_watchers_.erase(id)) {
LOG(ERROR) << "Couldn't find keyboard watcher with ID " << id
<< " when trying to unforward input.";
}
if (IsLoggingEnabled()) {
DiagnosticsLogController::Get()
->GetKeyboardInputLog()
.CreateLogAndRemoveKeyboard(id);
}
healthd_event_reporter_.ReportKeyboardDiagnosticEvent(id, keyboards_[id]);
// If there are no more watchers, unblock shortcuts
if (keyboard_watchers_.empty()) {
BlockShortcuts(/*should_block=*/false);
}
metrics::EmitKeyboardTesterRoutineDuration(base::Time::Now() -
keyboard_tester_start_timestamp_);
}
const std::string InputDataProvider::GetKeyboardName(uint32_t id) {
auto iter = keyboards_.find(id);
return iter == keyboards_.end() ? "" : iter->second->name;
}
void InputDataProvider::OnObservedKeyboardInputDisconnect(
uint32_t id,
mojo::RemoteSetElementId) {
if (!keyboard_observers_.contains(id)) {
LOG(ERROR) << "received keyboard observer disconnect for ID " << id
<< " without observer.";
return;
}
// When the last observer has been disconnected, stop forwarding events.
if (keyboard_observers_[id]->empty()) {
keyboard_observers_.erase(id);
UnforwardKeyboardInput(id);
// The observer RemoteSet remains empty at this point; if a new
// observer comes in, we will Forward it again.
}
}
void InputDataProvider::ObserveKeyEvents(
uint32_t id,
mojo::PendingRemote<mojom::KeyboardObserver> observer) {
CHECK(widget_) << "Observing Key Events for input diagnostics not allowed "
"without widget to track focus.";
if (!keyboards_.contains(id)) {
LOG(ERROR) << "Couldn't find keyboard with ID " << id
<< " when trying to receive input.";
return;
}
if (IsLoggingEnabled()) {
DiagnosticsLogController::Get()->GetKeyboardInputLog().AddKeyboard(
id, GetKeyboardName(id));
}
// When keyboard observer remote set is constructed, establish the
// disconnect handler.
if (!keyboard_observers_.contains(id)) {
keyboard_observers_[id] =
std::make_unique<mojo::RemoteSet<mojom::KeyboardObserver>>();
keyboard_observers_[id]->set_disconnect_handler(base::BindRepeating(
&InputDataProvider::OnObservedKeyboardInputDisconnect,
base::Unretained(this), id));
}
auto& observers = *keyboard_observers_[id];
const auto observer_id = observers.Add(std::move(observer));
// Ensure first callback is 'Paused' if we do not currently have focus
if (!may_send_events_)
observers.Get(observer_id)->OnKeyEventsPaused();
// When we are adding the first observer, start forwarding events.
if (observers.size() == 1)
ForwardKeyboardInput(id);
}
void InputDataProvider::SendPauseEvents() {
for (const auto& keyboard : keyboard_observers_) {
for (const auto& observer : *keyboard.second) {
observer->OnKeyEventsPaused();
}
}
// Re-arm our log message for future events.
logged_not_dispatching_key_events_ = false;
}
void InputDataProvider::SendResumeEvents() {
for (const auto& keyboard : keyboard_observers_) {
for (const auto& observer : *keyboard.second) {
observer->OnKeyEventsResumed();
}
}
}
void InputDataProvider::OnDeviceEvent(const ui::DeviceEvent& event) {
if (event.device_type() != ui::DeviceEvent::DeviceType::INPUT ||
event.action_type() == ui::DeviceEvent::ActionType::CHANGE) {
return;
}
int id = -1;
if (!GetEventNodeId(event.path(), &id)) {
LOG(ERROR) << "Ignoring DeviceEvent: invalid path " << event.path();
return;
}
if (event.action_type() == ui::DeviceEvent::ActionType::ADD) {
info_helper_.AsyncCall(&InputDeviceInfoHelper::GetDeviceInfo)
.WithArgs(id, event.path())
.Then(base::BindOnce(&InputDataProvider::ProcessDeviceInfo,
weak_factory_.GetWeakPtr()));
} else {
DCHECK(event.action_type() == ui::DeviceEvent::ActionType::REMOVE);
if (keyboards_.contains(id)) {
if (keyboard_observers_.erase(id)) {
// Unref'ing the observers does not trigger their
// OnObservedKeyboardInputDisconnect handlers (which would normally
// clean up any watchers), so we must explicitly release the watchers
// here.
UnforwardKeyboardInput(id);
}
keyboards_.erase(id);
keyboard_aux_data_.erase(id);
for (const auto& observer : connected_devices_observers_) {
observer->OnKeyboardDisconnected(id);
}
} else if (touch_devices_.contains(id)) {
touch_devices_.erase(id);
for (const auto& observer : connected_devices_observers_) {
observer->OnTouchDeviceDisconnected(id);
}
}
}
}
void InputDataProvider::ProcessDeviceInfo(
std::unique_ptr<InputDeviceInformation> device_info) {
if (device_info == nullptr) {
return;
}
if (IsTouchInputDevice(device_info.get())) {
AddTouchDevice(device_info.get());
} else if (device_info->event_device_info.HasKeyboard()) {
AddKeyboard(device_info.get());
} else if (device_info->event_device_info.HasSwEvent(SW_TABLET_MODE)) {
// Having a tablet mode switch indicates that this is a convertible, so
// the top-right key of the keyboard is most likely to be lock.
has_tablet_mode_switch_ = true;
// Since this device might be processed after the internal keyboard,
// update any internal keyboards that are already registered (except ones
// which we know have Control Panel on the top-right key).
for (const auto& keyboard_pair : keyboards_) {
const mojom::KeyboardInfoPtr& keyboard = keyboard_pair.second;
if (keyboard->connection_type == mojom::ConnectionType::kInternal &&
keyboard->top_right_key != mojom::TopRightKey::kControlPanel) {
keyboard->top_right_key = mojom::TopRightKey::kLock;
}
}
}
}
void InputDataProvider::AddTouchDevice(
const InputDeviceInformation* device_info) {
touch_devices_[device_info->evdev_id] =
touch_helper_.ConstructTouchDevice(device_info, is_internal_display_on_);
for (const auto& observer : connected_devices_observers_) {
observer->OnTouchDeviceConnected(
touch_devices_[device_info->evdev_id]->Clone());
}
}
void InputDataProvider::AddKeyboard(const InputDeviceInformation* device_info) {
auto aux_data = std::make_unique<InputDataProviderKeyboard::AuxData>();
mojom::KeyboardInfoPtr keyboard =
keyboard_helper_.ConstructKeyboard(device_info, aux_data.get());
const bool is_internal_keyboard =
keyboard->connection_type == mojom::ConnectionType::kInternal;
// Don't add keyboard if internal keyboard is a split modifier keyboard.
if (is_internal_keyboard &&
IsSplitModifierKeyboard(device_info->input_device.id)) {
return;
}
if (!features::IsExternalKeyboardInDiagnosticsAppEnabled() &&
!is_internal_keyboard) {
return;
}
keyboards_[device_info->evdev_id] = std::move(keyboard);
if (device_info->connection_type == mojom::ConnectionType::kInternal &&
keyboards_[device_info->evdev_id]->top_right_key ==
mojom::TopRightKey::kUnknown) {
// With some exceptions, convertibles (with tablet mode switches) tend to
// have a lock key in the top-right of the keyboard, while clamshells tend
// to have a power key.
keyboards_[device_info->evdev_id]->top_right_key =
has_tablet_mode_switch_ ? mojom::TopRightKey::kLock
: mojom::TopRightKey::kPower;
}
keyboard_aux_data_[device_info->evdev_id] = std::move(aux_data);
for (const auto& observer : connected_devices_observers_) {
observer->OnKeyboardConnected(keyboards_[device_info->evdev_id]->Clone());
}
// Check if get_connected_devices_callback_ needs to be executed.
if (is_internal_keyboard && !get_connected_devices_callback_.is_null()) {
std::move(get_connected_devices_callback_).Run();
}
}
void InputDataProvider::SendInputKeyEvent(uint32_t id,
uint32_t key_code,
uint32_t scan_code,
bool down) {
CHECK(widget_) << "Sending Key Events for input diagnostics not allowed "
"without widget to track focus.";
if (!keyboard_observers_.contains(id)) {
LOG(ERROR) << "Couldn't find keyboard observer with ID " << id
<< " when trying to dispatch key.";
return;
}
if (!may_send_events_) {
if (!logged_not_dispatching_key_events_) {
// Note: this will be common if the input diagnostics window is opened,
// but not focused, so just log once.
LOG(ERROR) << "Will not dispatch keys when diagnostics window does not "
"have focus.";
logged_not_dispatching_key_events_ = true;
}
return;
}
mojom::KeyEventPtr event = keyboard_helper_.ConstructInputKeyEvent(
keyboards_[id], keyboard_aux_data_[id].get(), key_code, scan_code, down);
if (IsLoggingEnabled()) {
DiagnosticsLogController::Get()
->GetKeyboardInputLog()
.RecordKeyPressForKeyboard(id, event.Clone());
}
healthd_event_reporter_.AddKeyEventForNextReport(id, event);
const auto& observers = *keyboard_observers_[id];
for (const auto& observer : observers) {
observer->OnKeyEvent(event->Clone());
}
}
} // namespace diagnostics
} // namespace ash