chromium/remoting/host/touch_injector_win.cc

// Copyright 2015 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/touch_injector_win.h"

#include <string>
#include <utility>

#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/native_library.h"
#include "base/notreached.h"
#include "base/time/time.h"
#include "remoting/proto/event.pb.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capture_types.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h"
#include "third_party/webrtc/modules/desktop_capture/win/screen_capture_utils.h"

namespace remoting {

using protocol::TouchEvent;
using protocol::TouchEventPoint;

namespace {

typedef BOOL(NTAPI* InitializeTouchInjectionFunction)(UINT32, DWORD);
typedef BOOL(NTAPI* InjectTouchInputFunction)(UINT32,
                                              const POINTER_TOUCH_INFO*);
const uint32_t kMaxSimultaneousTouchCount = 10;

// This is used to reinject all points that have not changed as "move"ed points,
// even if they have not actually moved.
// This is required for multi-touch to work, e.g. pinching and zooming gestures
// (handled by apps) won't work without reinjecting the points, even though the
// user moved only one finger and held the other finger in place.
void AppendMapValuesToVector(
    std::map<uint32_t, POINTER_TOUCH_INFO>* touches_in_contact,
    std::vector<POINTER_TOUCH_INFO>* output_vector) {
  for (auto& id_and_pointer_touch_info : *touches_in_contact) {
    POINTER_TOUCH_INFO& pointer_touch_info = id_and_pointer_touch_info.second;
    output_vector->push_back(pointer_touch_info);
  }
}

void ConvertToPointerTouchInfoImpl(const TouchEventPoint& touch_point,
                                   POINTER_TOUCH_INFO* pointer_touch_info) {
  pointer_touch_info->touchMask =
      TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION;
  pointer_touch_info->touchFlags = TOUCH_FLAG_NONE;

  // Although radius_{x,y} can be undefined (i.e. has_radius_{x,y} == false),
  // the default value (0.0) will set the area correctly.
  // MSDN mentions that if the digitizer does not detect the size of the touch
  // point, rcContact should be set to 0 by 0 rectangle centered at the
  // coordinate.
  pointer_touch_info->rcContact.left = touch_point.x() - touch_point.radius_x();
  pointer_touch_info->rcContact.top = touch_point.y() - touch_point.radius_y();
  pointer_touch_info->rcContact.right =
      touch_point.x() + touch_point.radius_x();
  pointer_touch_info->rcContact.bottom =
      touch_point.y() + touch_point.radius_y();

  pointer_touch_info->orientation = touch_point.angle();

  if (touch_point.has_pressure()) {
    pointer_touch_info->touchMask |= TOUCH_MASK_PRESSURE;
    const float kMinimumPressure = 0.0;
    const float kMaximumPressure = 1.0;
    const float clamped_touch_point_pressure = std::max(
        kMinimumPressure, std::min(kMaximumPressure, touch_point.pressure()));

    const int kWindowsMaxTouchPressure = 1024;  // Defined in MSDN.
    const int pressure =
        clamped_touch_point_pressure * kWindowsMaxTouchPressure;
    pointer_touch_info->pressure = pressure;
  }

  pointer_touch_info->pointerInfo.pointerType = PT_TOUCH;
  pointer_touch_info->pointerInfo.pointerId = touch_point.id();
  pointer_touch_info->pointerInfo.ptPixelLocation.x = touch_point.x();
  pointer_touch_info->pointerInfo.ptPixelLocation.y = touch_point.y();
}

// The caller should set memset(0) the struct and set
// pointer_touch_info->pointerInfo.pointerFlags.
void ConvertToPointerTouchInfo(const TouchEventPoint& touch_point,
                               POINTER_TOUCH_INFO* pointer_touch_info) {
  // TODO(zijiehe): Use GetFullscreenTopLeft() once
  // https://chromium-review.googlesource.com/c/581951/ is submitted.
  webrtc::DesktopVector top_left =
      webrtc::GetScreenRect(webrtc::kFullDesktopScreenId, std::wstring())
          .top_left();
  if (top_left.is_zero()) {
    ConvertToPointerTouchInfoImpl(touch_point, pointer_touch_info);
    return;
  }

  TouchEventPoint point(touch_point);
  point.set_x(point.x() + top_left.x());
  point.set_y(point.y() + top_left.y());

  ConvertToPointerTouchInfoImpl(point, pointer_touch_info);
}

}  // namespace

TouchInjectorWinDelegate::~TouchInjectorWinDelegate() {}

// static.
std::unique_ptr<TouchInjectorWinDelegate> TouchInjectorWinDelegate::Create() {
  base::ScopedNativeLibrary library(base::FilePath(L"User32.dll"));
  if (!library.is_valid()) {
    PLOG(INFO) << "Failed to get library module for touch injection functions.";
    return nullptr;
  }

  InitializeTouchInjectionFunction init_func =
      reinterpret_cast<InitializeTouchInjectionFunction>(
          library.GetFunctionPointer("InitializeTouchInjection"));
  if (!init_func) {
    PLOG(INFO) << "Failed to get InitializeTouchInjection function handle.";
    return nullptr;
  }

  InjectTouchInputFunction inject_touch_func =
      reinterpret_cast<InjectTouchInputFunction>(
          library.GetFunctionPointer("InjectTouchInput"));
  if (!inject_touch_func) {
    PLOG(INFO) << "Failed to get InjectTouchInput.";
    return nullptr;
  }

  return std::unique_ptr<TouchInjectorWinDelegate>(new TouchInjectorWinDelegate(
      library.release(), init_func, inject_touch_func));
}

TouchInjectorWinDelegate::TouchInjectorWinDelegate(
    base::NativeLibrary library,
    InitializeTouchInjectionFunction initialize_touch_injection_func,
    InjectTouchInputFunction inject_touch_input_func)
    : library_module_(library),
      initialize_touch_injection_func_(initialize_touch_injection_func),
      inject_touch_input_func_(inject_touch_input_func) {}

BOOL TouchInjectorWinDelegate::InitializeTouchInjection(UINT32 max_count,
                                                        DWORD dw_mode) {
  return initialize_touch_injection_func_(max_count, dw_mode);
}

DWORD TouchInjectorWinDelegate::InjectTouchInput(
    UINT32 count,
    const POINTER_TOUCH_INFO* contacts) {
  return inject_touch_input_func_(count, contacts);
}

TouchInjectorWin::TouchInjectorWin() = default;

TouchInjectorWin::~TouchInjectorWin() = default;

// Note that TouchInjectorWinDelegate::Create() is not called in this method
// so that a mock delegate can be injected in tests and set expectations on the
// mock and return value of this method.
bool TouchInjectorWin::Init() {
  if (!delegate_) {
    delegate_ = TouchInjectorWinDelegate::Create();
  }

  // If initializing the delegate failed above, then the platform likely doesn't
  // support touch (or the libraries failed to load for some reason).
  if (!delegate_) {
    return false;
  }

  if (!delegate_->InitializeTouchInjection(kMaxSimultaneousTouchCount,
                                           TOUCH_FEEDBACK_DEFAULT)) {
    // delagate_ is reset here so that the function that need the delegate
    // can check if it is null.
    delegate_.reset();
    PLOG(INFO) << "Failed to initialize touch injection.";
    return false;
  }

  return true;
}

void TouchInjectorWin::Deinitialize() {
  touches_in_contact_.clear();
  // Same reason as TouchInjectorWin::Init(). For injecting mock delegates for
  // tests, a new delegate is created here.
  delegate_ = TouchInjectorWinDelegate::Create();
  last_injected_time_ = base::TimeTicks();
  keep_alive_timer_.Stop();
}

void TouchInjectorWin::InjectTouchEvent(const TouchEvent& event) {
  if (!delegate_) {
    VLOG(3) << "Touch injection functions are not initialized.";
    return;
  }

  switch (event.event_type()) {
    case TouchEvent::TOUCH_POINT_START:
      AddNewTouchPoints(event);
      break;
    case TouchEvent::TOUCH_POINT_MOVE:
      MoveTouchPoints(event);
      break;
    case TouchEvent::TOUCH_POINT_END:
      EndTouchPoints(event);
      break;
    case TouchEvent::TOUCH_POINT_CANCEL:
      CancelTouchPoints(event);
      break;
    default:
      NOTREACHED_IN_MIGRATION();
      return;
  }
}

void TouchInjectorWin::SetInjectorDelegateForTest(
    std::unique_ptr<TouchInjectorWinDelegate> functions) {
  delegate_ = std::move(functions);
}

void TouchInjectorWin::AddNewTouchPoints(const TouchEvent& event) {
  DCHECK_EQ(event.event_type(), TouchEvent::TOUCH_POINT_START);

  std::vector<POINTER_TOUCH_INFO> touches;
  // Must inject already touching points as move events.
  AppendMapValuesToVector(&touches_in_contact_, &touches);

  for (const TouchEventPoint& touch_point : event.touch_points()) {
    POINTER_TOUCH_INFO pointer_touch_info;
    memset(&pointer_touch_info, 0, sizeof(pointer_touch_info));
    pointer_touch_info.pointerInfo.pointerFlags =
        POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN;
    ConvertToPointerTouchInfo(touch_point, &pointer_touch_info);
    touches.push_back(pointer_touch_info);

    // All points in the map should be a move point.
    pointer_touch_info.pointerInfo.pointerFlags =
        POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_UPDATE;
    touches_in_contact_[touch_point.id()] = pointer_touch_info;
  }

  if (!InjectTouchInput(touches)) {
    PLOG(ERROR) << "Failed to inject a touch start event.";
  }
}

void TouchInjectorWin::MoveTouchPoints(const TouchEvent& event) {
  DCHECK_EQ(event.event_type(), TouchEvent::TOUCH_POINT_MOVE);

  for (const TouchEventPoint& touch_point : event.touch_points()) {
    POINTER_TOUCH_INFO* pointer_touch_info =
        &touches_in_contact_[touch_point.id()];
    memset(pointer_touch_info, 0, sizeof(*pointer_touch_info));
    pointer_touch_info->pointerInfo.pointerFlags =
        POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_UPDATE;
    ConvertToPointerTouchInfo(touch_point, pointer_touch_info);
  }

  std::vector<POINTER_TOUCH_INFO> touches;
  // Must inject already touching points as move events.
  AppendMapValuesToVector(&touches_in_contact_, &touches);
  if (!InjectTouchInput(touches)) {
    PLOG(ERROR) << "Failed to inject a touch move event.";
  }
}

void TouchInjectorWin::EndTouchPoints(const TouchEvent& event) {
  DCHECK_EQ(event.event_type(), TouchEvent::TOUCH_POINT_END);

  std::vector<POINTER_TOUCH_INFO> touches;
  for (const TouchEventPoint& touch_point : event.touch_points()) {
    POINTER_TOUCH_INFO pointer_touch_info =
        touches_in_contact_[touch_point.id()];
    pointer_touch_info.pointerInfo.pointerFlags = POINTER_FLAG_UP;

    touches_in_contact_.erase(touch_point.id());
    touches.push_back(pointer_touch_info);
  }

  AppendMapValuesToVector(&touches_in_contact_, &touches);
  if (!InjectTouchInput(touches)) {
    PLOG(ERROR) << "Failed to inject a touch end event.";
  }
}

void TouchInjectorWin::CancelTouchPoints(const TouchEvent& event) {
  DCHECK_EQ(event.event_type(), TouchEvent::TOUCH_POINT_CANCEL);

  std::vector<POINTER_TOUCH_INFO> touches;
  for (const TouchEventPoint& touch_point : event.touch_points()) {
    POINTER_TOUCH_INFO pointer_touch_info =
        touches_in_contact_[touch_point.id()];
    pointer_touch_info.pointerInfo.pointerFlags =
        POINTER_FLAG_UP | POINTER_FLAG_CANCELED;

    touches_in_contact_.erase(touch_point.id());
    touches.push_back(pointer_touch_info);
  }

  AppendMapValuesToVector(&touches_in_contact_, &touches);
  if (!InjectTouchInput(touches)) {
    PLOG(ERROR) << "Failed to inject a touch cancel event.";
  }
}

bool TouchInjectorWin::InjectTouchInput(
    const std::vector<POINTER_TOUCH_INFO>& touches) {
  if (delegate_->InjectTouchInput(touches.size(), touches.data()) == 0) {
    return false;
  }
  last_injected_time_ = base::TimeTicks::Now();
  UpdateKeepAliveTimer();
  return true;
}

void TouchInjectorWin::UpdateKeepAliveTimer() {
  if (touches_in_contact_.empty()) {
    keep_alive_timer_.Stop();
    return;
  }
  if (!keep_alive_timer_.IsRunning()) {
    keep_alive_timer_.Start(FROM_HERE, kKeepAliveInterval, this,
                            &TouchInjectorWin::OnKeepAlive);
  }
}

void TouchInjectorWin::OnKeepAlive() {
  if ((base::TimeTicks::Now() - last_injected_time_) < kKeepAliveInterval) {
    return;
  }
  DCHECK(!touches_in_contact_.empty());
  std::vector<POINTER_TOUCH_INFO> touches;
  AppendMapValuesToVector(&touches_in_contact_, &touches);
  if (!InjectTouchInput(touches)) {
    PLOG(ERROR) << "Failed to inject a keep-alive touch move event.";
  }
}

}  // namespace remoting