chromium/remoting/host/input_injector_chromeos_unittest.cc

// 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