blob: 5be81084158bfd5c0ffbafa803804840124a1800 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "remoting/host/linux/gnome_input_injector.h"
#include "base/memory/weak_ptr.h"
#include "base/notimplemented.h"
#include "base/strings/utf_string_conversion_utils.h"
#include "remoting/base/logging.h"
#include "remoting/host/linux/ei_keymap.h"
#include "remoting/host/linux/ei_sender_session.h"
#include "remoting/host/linux/pipewire_capture_stream.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capture_types.h"
namespace remoting {
GnomeInputInjector::GnomeInputInjector(
base::WeakPtr<EiSenderSession> session,
base::WeakPtr<const PipewireCaptureStreamManager> stream_manager,
GDBusConnectionRef dbus_connection,
gvariant::ObjectPath session_path)
: ei_session_(session),
stream_manager_(stream_manager),
clipboard_(std::move(dbus_connection), std::move(session_path)) {}
GnomeInputInjector::~GnomeInputInjector() = default;
base::WeakPtr<GnomeInputInjector> GnomeInputInjector::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
void GnomeInputInjector::SetKeymap(base::WeakPtr<EiKeymap> keymap) {
keymap_ = keymap;
}
void GnomeInputInjector::Start(
std::unique_ptr<protocol::ClipboardStub> client_clipboard) {
clipboard_.Start(std::move(client_clipboard));
}
void GnomeInputInjector::InjectKeyEvent(const protocol::KeyEvent& event) {
if (!ei_session_ || !keymap_) {
return;
}
if (!event.has_usb_keycode() || !event.has_pressed()) {
LOG(WARNING) << "Key event with no key info";
return;
}
if (!event.pressed()) {
// If the key isn't pressed, there's nothing to do. This is expected if
// the key was released immediately after being pressed in order to avoid
// unwanted auto-repeat.
if (!pressed_keys_.erase(event.usb_keycode())) {
return;
}
}
ei_session_->InjectKeyEvent(event.usb_keycode(), event.pressed());
if (event.pressed()) {
// Immediately release non-modifier keys to avoid unwanted auto-repeat.
//
// TODO: jamiewalch - Remove this workaround once
// https://gitlab.freedesktop.org/libinput/libei/-/issues/74 is fixed.
if (keymap_->CanAutoRepeatUsbCode(event.usb_keycode())) {
pressed_keys_.insert(event.usb_keycode());
} else {
ei_session_->InjectKeyEvent(event.usb_keycode(), false);
}
}
}
void GnomeInputInjector::InjectTextEvent(const protocol::TextEvent& event) {
if (!keymap_) {
return;
}
if (!ei_session_) {
return;
}
// Release all keys before injecting text event. This is necessary to avoid
// any interference with the currently pressed keys. E.g. if Shift is pressed
// when TextEvent is received.
for (const auto key : pressed_keys_) {
ei_session_->InjectKeyEvent(key, false);
}
pressed_keys_.clear();
std::vector<EiKeymap::Recipe> recipes;
const std::string& text = event.text();
for (size_t index = 0; index < text.size(); ++index) {
base_icu::UChar32 code_point;
if (!base::ReadUnicodeCharacter(text.c_str(), text.size(), &index,
&code_point)) {
LOG(ERROR) << "Invalid encoding at index: " << index
<< " for text: " << text << ". Not injecting any text.";
return;
}
auto recipe = keymap_->GetRecipeForCodepoint(code_point);
// Skip unsupported codepoints. Ideally we'd ignore the whole string in this
// case so that it's obvious to the user that something is wrong, but since
// the client splits the string into individual characters it's not possible
// to know when two text events are part of the same entry.
if (recipe.usb_code == 0) {
LOG(WARNING) << "Unsupported codepoint: " << code_point
<< " for text: " << text;
continue;
}
recipes.push_back(recipe);
}
std::set<uint32_t> pressed_modifiers;
for (const auto& recipe : recipes) {
// Release any modifiers that are no longer needed.
for (const auto key : pressed_modifiers) {
if (!recipe.modifiers.contains(key)) {
ei_session_->InjectKeyEvent(key, false);
}
}
// Press any new modifiers that are now needed.
for (const auto key : recipe.modifiers) {
if (!pressed_modifiers.contains(key)) {
ei_session_->InjectKeyEvent(key, true);
}
}
pressed_modifiers = recipe.modifiers;
ei_session_->InjectKeyEvent(recipe.usb_code, true);
ei_session_->InjectKeyEvent(recipe.usb_code, false);
}
for (const auto key : pressed_modifiers) {
ei_session_->InjectKeyEvent(key, false);
}
}
void GnomeInputInjector::InjectMouseEvent(const protocol::MouseEvent& event) {
if (!ei_session_) {
return;
}
bool event_sent = false;
if (event.has_fractional_coordinate() &&
event.fractional_coordinate().has_x() &&
event.fractional_coordinate().has_y()) {
if (!stream_manager_) {
LOG(WARNING) << "PipewireCaptureStreamManager no longer exists.";
} else {
webrtc::ScreenId screen_id = event.fractional_coordinate().screen_id();
base::WeakPtr<const CaptureStream> stream =
stream_manager_->GetStream(screen_id);
if (!stream) {
LOG(ERROR) << "Unexpected screen ID: " << screen_id;
} else {
ei_session_->InjectAbsolutePointerMove(
stream->mapping_id(), event.fractional_coordinate().x(),
event.fractional_coordinate().y());
event_sent = true;
}
}
} else if (event.has_delta_x() || event.has_delta_y()) {
ei_session_->InjectRelativePointerMove(
event.has_delta_x() ? event.delta_x() : 0,
event.has_delta_y() ? event.delta_y() : 0);
event_sent = true;
}
if (event.has_button() && event.has_button_down()) {
ei_session_->InjectButton(event.button(), event.button_down());
event_sent = true;
}
if (event.has_wheel_ticks_x() || event.has_wheel_ticks_y()) {
ei_session_->InjectScrollDiscrete(
event.has_wheel_ticks_x() ? event.wheel_ticks_x() : 0,
event.has_wheel_ticks_y() ? event.wheel_ticks_y() : 0);
event_sent = true;
} else if (event.has_wheel_delta_x() || event.has_wheel_delta_y()) {
ei_session_->InjectScrollDelta(
event.has_wheel_delta_x() ? event.wheel_delta_x() : 0,
event.has_wheel_delta_y() ? event.wheel_delta_y() : 0);
event_sent = true;
}
if (!event_sent) {
LOG(WARNING) << "No mouse event is injected.";
}
}
void GnomeInputInjector::InjectTouchEvent(const protocol::TouchEvent& event) {
NOTIMPLEMENTED();
}
void GnomeInputInjector::InjectClipboardEvent(
const protocol::ClipboardEvent& event) {
clipboard_.InjectClipboardEvent(event);
}
} // namespace remoting