| // Copyright 2023 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/input_injector_chromeos.h" |
| |
| #include "ash/shell.h" |
| #include "ash/test/ash_test_base.h" |
| #include "base/check_deref.h" |
| #include "base/test/test_future.h" |
| #include "chromeos/ash/components/test/ash_test_suite.h" |
| #include "remoting/proto/event.pb.h" |
| #include "remoting/protocol/protocol_mock_objects.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/aura/window_tree_host.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/display/manager/display_manager.h" |
| #include "ui/display/test/display_manager_test_api.h" |
| #include "ui/events/event_constants.h" |
| #include "ui/ozone/public/system_input_injector.h" |
| |
| namespace remoting { |
| |
| namespace { |
| |
| // The index used by `DisplayManager` to distinguish the displays, which is |
| // simply a zero-based index and *not* the `display.id()` field! |
| enum DisplayIndex { |
| kFirstDisplay = 0, |
| kSecondDisplay = 1, |
| }; |
| |
| protocol::MouseEvent MakeMouseMoveEvent(int x, int y) { |
| protocol::MouseEvent result; |
| result.set_x(x); |
| result.set_y(y); |
| return result; |
| } |
| |
| protocol::MouseEvent MakeLeftClickEvent(int x, int y) { |
| protocol::MouseEvent result; |
| result.set_x(x); |
| result.set_y(y); |
| result.set_button_down(true); |
| result.set_button(protocol::MouseEvent::BUTTON_LEFT); |
| return result; |
| } |
| |
| gfx::PointF PointFWithOffset(float x, float y, gfx::Point offset) { |
| gfx::PointF result{x, y}; |
| result.Offset(offset.x(), offset.y()); |
| return result; |
| } |
| |
| gfx::Point PointWithOffset(int x, int y, gfx::Point offset) { |
| gfx::Point result{x, y}; |
| result.Offset(offset.x(), offset.y()); |
| return result; |
| } |
| |
| class FakeSystemInputInjector : public ui::SystemInputInjector { |
| public: |
| FakeSystemInputInjector() = default; |
| FakeSystemInputInjector(const FakeSystemInputInjector&) = delete; |
| FakeSystemInputInjector& operator=(const FakeSystemInputInjector&) = delete; |
| ~FakeSystemInputInjector() override = default; |
| |
| // `SystemInputInjector` implementation: |
| void SetDeviceId(int device_id) override { |
| device_id_future_.SetValue(device_id); |
| } |
| void MoveCursorTo(const gfx::PointF& location) override { |
| cursor_moves_.SetValue(location); |
| } |
| void InjectMouseButton(ui::EventFlags button, bool down) override { |
| mouse_button_event_.SetValue(button); |
| } |
| void InjectMouseWheel(int delta_x, int delta_y) override {} |
| void InjectKeyEvent(ui::DomCode physical_key, |
| bool down, |
| bool suppress_auto_repeat) override {} |
| |
| int WaitForDeviceId() { return device_id_future_.Get(); } |
| |
| gfx::PointF NextCursorMove() { return cursor_moves_.Take(); } |
| |
| ui::EventFlags WaitForMouseButtonEvent() { |
| return mouse_button_event_.Take(); |
| } |
| |
| private: |
| base::test::TestFuture<int> device_id_future_; |
| base::test::TestFuture<gfx::PointF> cursor_moves_; |
| base::test::TestFuture<ui::EventFlags> mouse_button_event_; |
| }; |
| |
| } // namespace |
| |
| class InputInjectorChromeosTest : public ash::AshTestBase { |
| public: |
| InputInjectorChromeosTest() = default; |
| InputInjectorChromeosTest(const InputInjectorChromeosTest&) = delete; |
| InputInjectorChromeosTest& operator=(const InputInjectorChromeosTest&) = |
| delete; |
| ~InputInjectorChromeosTest() override = default; |
| |
| void SetUp() override { |
| // Unset the resource bundle set by our own test suite... |
| ui::ResourceBundle::CleanupSharedInstance(); |
| // ... since Ash requires that we load their resource bundle. |
| ash::AshTestSuite::LoadTestResources(); |
| ash::AshTestBase::SetUp(); |
| |
| input_injector_ = std::make_unique<InputInjectorChromeos>( |
| base::SingleThreadTaskRunner::GetCurrentDefault()); |
| |
| auto system_input_unique_ptr = std::make_unique<FakeSystemInputInjector>(); |
| delegate_ = system_input_unique_ptr.get(); |
| input_injector_->StartForTesting( |
| std::move(system_input_unique_ptr), |
| std::make_unique<protocol::MockClipboardStub>()); |
| } |
| |
| void TearDown() override { |
| delegate_ = nullptr; |
| input_injector_.reset(); |
| ash::AshTestBase::TearDown(); |
| } |
| |
| void CreateSingleDisplay(const std::string& display_specs) { |
| display::test::DisplayManagerTestApi(&display_manager()) |
| .UpdateDisplay(display_specs); |
| } |
| |
| void CreateMultipleDisplays(const std::string& first_display_specs, |
| const std::string& second_display_specs) { |
| display::test::DisplayManagerTestApi(&display_manager()) |
| .UpdateDisplay(first_display_specs + "," + second_display_specs); |
| } |
| |
| void InjectMouseMoveEvent(int x, int y) { |
| input_injector().InjectMouseEvent(MakeMouseMoveEvent(x, y)); |
| } |
| |
| void InjectMouseEvent(const protocol::MouseEvent& event) { |
| input_injector().InjectMouseEvent(event); |
| } |
| |
| void EnableUnifiedDesktop() { |
| display_manager().SetUnifiedDesktopEnabled(true); |
| EXPECT_EQ(display_manager().GetNumDisplays(), 1ull); |
| EXPECT_EQ(ash::Shell::Get()->GetAllRootWindows().size(), 1ull); |
| } |
| |
| // Even if a display has origin (0,0) in our DPI screen coordinates, |
| // its origin is likely not (0,0) in the Pixel screen coordinates. |
| gfx::Point get_origin_in_pixel_coordinates(DisplayIndex display) { |
| return ash::Shell::GetRootWindowForDisplayId( |
| display_manager().GetDisplayAt(display).id()) |
| ->GetHost() |
| ->GetBoundsInPixels() |
| .origin(); |
| } |
| |
| gfx::Point get_origin_in_screen_coordinates(DisplayIndex display) { |
| return display_manager().GetDisplayAt(display).bounds().origin(); |
| } |
| |
| // Convert the given relative location (in screen coordinates) to the |
| // absolute location (still in screen coordinates). |
| // In practice this means we have to add in the origin of the display. |
| gfx::Point PointInScreenIn(DisplayIndex display, int x, int y) { |
| return PointWithOffset(x, y, get_origin_in_pixel_coordinates(display)); |
| } |
| |
| // Convert the given relative location (in pixel coordinates) to the |
| // absolute location (still in pixel coordinates). |
| // In practice this means we have to add in the origin of the display. |
| gfx::PointF PointFInPixelIn(DisplayIndex display, int x, int y) { |
| return PointFWithOffset(x, y, get_origin_in_pixel_coordinates(display)); |
| } |
| |
| display::DisplayManager& display_manager() { |
| return CHECK_DEREF(ash::Shell::Get()->display_manager()); |
| } |
| |
| FakeSystemInputInjector& delegate() { return CHECK_DEREF(delegate_.get()); } |
| |
| InputInjectorChromeos& input_injector() { |
| return CHECK_DEREF(input_injector_.get()); |
| } |
| |
| private: |
| raw_ptr<FakeSystemInputInjector> delegate_; |
| std::unique_ptr<InputInjectorChromeos> input_injector_; |
| }; |
| |
| TEST_F(InputInjectorChromeosTest, ShouldUseRemoteInputDeviceId) { |
| EXPECT_EQ(delegate().WaitForDeviceId(), ui::ED_REMOTE_INPUT_DEVICE); |
| } |
| |
| TEST_F(InputInjectorChromeosTest, ShouldForwardMouseEvents) { |
| CreateSingleDisplay("2000x1000"); |
| |
| InjectMouseMoveEvent(100, 200); |
| |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kFirstDisplay, 100, 200)); |
| } |
| |
| TEST_F(InputInjectorChromeosTest, ShouldForwardMouseEventsOnSecondScreen) { |
| CreateMultipleDisplays("750x250", "2000x1000"); |
| |
| InjectMouseMoveEvent(750 + 500, 200); |
| |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kSecondDisplay, 500, 200)); |
| } |
| |
| TEST_F(InputInjectorChromeosTest, ShouldForwardMouseEventsOnScreenEdges) { |
| CreateMultipleDisplays("1080x720", "2000x1000"); |
| |
| InjectMouseMoveEvent(0, 0); |
| EXPECT_EQ(delegate().NextCursorMove(), PointFInPixelIn(kFirstDisplay, 0, 0)); |
| |
| InjectMouseMoveEvent(1079, 0); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kFirstDisplay, 1079, 0)); |
| |
| InjectMouseMoveEvent(0, 719); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kFirstDisplay, 0, 719)); |
| |
| InjectMouseMoveEvent(1079, 719); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kFirstDisplay, 1079, 719)); |
| } |
| |
| TEST_F(InputInjectorChromeosTest, |
| ShouldForwardMouseEventsOnScreenEdgesOfSecondaryDisplay) { |
| CreateMultipleDisplays("1000x500", "1920x1080"); |
| |
| const int left_display_width = 1000; |
| |
| InjectMouseMoveEvent(left_display_width + 0, 0); |
| EXPECT_EQ(delegate().NextCursorMove(), // |
| PointFInPixelIn(kSecondDisplay, 0, 0)); |
| |
| InjectMouseMoveEvent(left_display_width + 1919, 0); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kSecondDisplay, 1919, 0)); |
| |
| InjectMouseMoveEvent(left_display_width + 0, 1079); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kSecondDisplay, 0, 1079)); |
| |
| InjectMouseMoveEvent(left_display_width + 1919, 1079); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kSecondDisplay, 1919, 1079)); |
| } |
| |
| TEST_F(InputInjectorChromeosTest, ShouldUseCorrectPositionForMouseClick) { |
| CreateSingleDisplay("2000x1000"); |
| |
| InjectMouseEvent(MakeLeftClickEvent(100, 200)); |
| |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kFirstDisplay, 100, 200)); |
| |
| EXPECT_EQ(delegate().WaitForMouseButtonEvent(), ui::EF_LEFT_MOUSE_BUTTON); |
| } |
| |
| TEST_F(InputInjectorChromeosTest, ShouldSupportLeftRotation) { |
| // `/l` rotates 90 degrees to the left. |
| CreateSingleDisplay("2000x1000/l"); |
| |
| // The input display has dimensions 1000x2000 after the rotation. |
| // On the other hand, the output coordinates are in pixel coordinates so |
| // they do *not* take rotation into account, meaning the output still |
| // needs to use the 2000x1000 resolution. |
| |
| InjectMouseMoveEvent(0, 0); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kFirstDisplay, 0, 1000)); |
| |
| InjectMouseMoveEvent(1000, 0); |
| EXPECT_EQ(delegate().NextCursorMove(), PointFInPixelIn(kFirstDisplay, 0, 0)); |
| |
| InjectMouseMoveEvent(0, 2000); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kFirstDisplay, 2000, 1000)); |
| |
| InjectMouseMoveEvent(1000, 2000); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kFirstDisplay, 2000, 0)); |
| } |
| |
| TEST_F(InputInjectorChromeosTest, ShouldSupportRightRotation) { |
| // `/r` rotates 90 degrees to the right. |
| CreateSingleDisplay("2000x1000/r"); |
| |
| // The input display has dimensions 1000x2000 after the rotation. |
| // On the other hand, the output coordinates are in pixel coordinates so |
| // they do *not* take rotation into account, meaning the output still |
| // needs to use the 2000x1000 resolution. |
| |
| InjectMouseMoveEvent(0, 0); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kFirstDisplay, 2000, 0)); |
| |
| InjectMouseMoveEvent(1000, 0); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kFirstDisplay, 2000, 1000)); |
| |
| InjectMouseMoveEvent(0, 2000); |
| EXPECT_EQ(delegate().NextCursorMove(), PointFInPixelIn(kFirstDisplay, 0, 0)); |
| |
| InjectMouseMoveEvent(1000, 2000); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kFirstDisplay, 0, 1000)); |
| } |
| |
| TEST_F(InputInjectorChromeosTest, ShouldSupportUpsideDownRotation) { |
| // `/u` rotates 180 degrees. |
| CreateSingleDisplay("2000x1000/u"); |
| |
| // The input display has dimensions 2000x1000 after the rotation. |
| |
| InjectMouseMoveEvent(0, 0); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kFirstDisplay, 2000, 1000)); |
| |
| InjectMouseMoveEvent(2000, 0); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kFirstDisplay, 0, 1000)); |
| |
| InjectMouseMoveEvent(0, 1000); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kFirstDisplay, 2000, 0)); |
| |
| InjectMouseMoveEvent(2000, 1000); |
| EXPECT_EQ(delegate().NextCursorMove(), PointFInPixelIn(kFirstDisplay, 0, 0)); |
| } |
| |
| TEST_F(InputInjectorChromeosTest, |
| ShouldSupportDisplayRotationOnSecondaryScreen) { |
| // `/l` rotates 90 degrees to the left. |
| CreateMultipleDisplays("5000x500", "2000x1000/l"); |
| |
| constexpr int left_display_width = 5000; |
| |
| // The input display has dimensions 1000x2000 after the rotation. |
| |
| InjectMouseMoveEvent(left_display_width + 0, 0); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kSecondDisplay, 0, 1000)); |
| |
| InjectMouseMoveEvent(left_display_width + 1000, 0); |
| EXPECT_EQ(delegate().NextCursorMove(), PointFInPixelIn(kSecondDisplay, 0, 0)); |
| |
| InjectMouseMoveEvent(left_display_width + 0, 2000); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kSecondDisplay, 2000, 1000)); |
| |
| InjectMouseMoveEvent(left_display_width + 1000, 2000); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kSecondDisplay, 2000, 0)); |
| } |
| |
| TEST_F(InputInjectorChromeosTest, ShouldSupportScaleFactor) { |
| // `@3` adds a scale factor of 3. |
| CreateSingleDisplay("3000x1500@3"); |
| |
| // The input display has dimensions 1000x500 after applying the scale factor. |
| |
| InjectMouseMoveEvent(300, 40); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kFirstDisplay, 300 * 3, 40 * 3)); |
| } |
| |
| TEST_F(InputInjectorChromeosTest, ShouldSupportScaleFactorWithRotation) { |
| CreateMultipleDisplays("3000x1500/l@3", "4000x2000/r@2"); |
| |
| // The first input display has dimensions 500x1000 after applying the scale |
| // factor and rotation. |
| // The second input display has dimensions 1000x2000 after applying the scale |
| // factor and rotation. |
| |
| constexpr int left_display_width = 500; |
| |
| InjectMouseMoveEvent(left_display_width + 60, 123); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFInPixelIn(kSecondDisplay, (2000 - 123) * 2, 60 * 2)); |
| } |
| |
| TEST_F(InputInjectorChromeosTest, ShouldSupportUnifiedDesktop) { |
| CreateMultipleDisplays("3000x1500", "3000x1500"); |
| |
| // Collect display offsets now because the APIs we use lose access to the |
| // real displays once unified desktop is enabled. |
| gfx::Point first_display_pixel_offset = |
| get_origin_in_pixel_coordinates(kFirstDisplay); |
| gfx::Point second_display_pixel_offset = |
| get_origin_in_pixel_coordinates(kSecondDisplay); |
| |
| EnableUnifiedDesktop(); |
| |
| InjectMouseMoveEvent(48, 127); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFWithOffset(48, 127, first_display_pixel_offset)); |
| |
| InjectMouseMoveEvent(3000 + 48, 127); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFWithOffset(48, 127, second_display_pixel_offset)); |
| } |
| |
| TEST_F(InputInjectorChromeosTest, |
| ShouldSupportUnifiedDesktopWithSingleDisplay) { |
| CreateSingleDisplay("3000x1500"); |
| |
| // Collect display offsets now because the APIs we use lose access to the |
| // real displays once unified desktop is enabled. |
| gfx::Point display_pixel_offset = |
| get_origin_in_pixel_coordinates(kFirstDisplay); |
| |
| EnableUnifiedDesktop(); |
| |
| InjectMouseMoveEvent(48, 127); |
| EXPECT_EQ(delegate().NextCursorMove(), |
| PointFWithOffset(48, 127, display_pixel_offset)); |
| } |
| |
| } // namespace remoting |