chromium/ash/display/touch_calibrator_controller.cc

// Copyright 2016 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/display/touch_calibrator_controller.h"

#include <memory>

#include "ash/display/touch_calibrator_view.h"
#include "ash/display/window_tree_host_manager.h"
#include "ash/host/ash_window_tree_host.h"
#include "ash/shell.h"
#include "ash/touch/ash_touch_transform_controller.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/ranges/algorithm.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "ui/aura/window_tree_host.h"
#include "ui/display/display.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/screen.h"
#include "ui/events/devices/device_data_manager.h"
#include "ui/events/event.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/size_conversions.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"

namespace ash {
namespace {

void InitInternalTouchDeviceIds(std::set<int>& internal_touch_device_ids) {
  internal_touch_device_ids.clear();
  const std::vector<ui::TouchscreenDevice>& device_list =
      ui::DeviceDataManager::GetInstance()->GetTouchscreenDevices();
  for (const auto& touchscreen_device : device_list) {
    if (touchscreen_device.type == ui::InputDeviceType::INPUT_DEVICE_INTERNAL) {
      internal_touch_device_ids.insert(touchscreen_device.id);
    }
  }
}

// Returns a transform to undo any transformations that are applied to events
// originating from the touch device identified with |touch_device_id|. This
// transform converts the event's location to the raw touch location.
gfx::Transform CalculateEventTransformer(int touch_device_id) {
  const display::DisplayManager* display_manager =
      Shell::Get()->display_manager();
  const std::vector<ui::TouchscreenDevice>& device_list =
      ui::DeviceDataManager::GetInstance()->GetTouchscreenDevices();

  auto device_it = base::ranges::find(device_list, touch_device_id,
                                      &ui::TouchscreenDevice::id);
  DCHECK(device_it != device_list.end())
      << "Device id " << touch_device_id
      << " is invalid. No such device connected to system";

  int64_t previous_display_id =
      display_manager->touch_device_manager()->GetAssociatedDisplay(*device_it);

  // If the touch device is not associated with any display. This may happen in
  // tests when the test does not setup the |ui::TouchDeviceTransform| before
  // generating a touch event.
  if (previous_display_id == display::kInvalidDisplayId) {
    return gfx::Transform();
  }

  // Undo the event transformations that the previous display applied on the
  // event location. We want to store the raw event location information.
  gfx::Transform tm =
      Shell::Get()
          ->window_tree_host_manager()
          ->GetAshWindowTreeHostForDisplayId(previous_display_id)
          ->AsWindowTreeHost()
          ->GetRootTransform();
  return tm;
}

}  // namespace

// Time interval after a touch event during which all other touch events are
// ignored during calibration.
const base::TimeDelta TouchCalibratorController::kTouchIntervalThreshold =
    base::Milliseconds(200);

TouchCalibratorController::TouchCalibratorController()
    : last_touch_timestamp_(base::Time::Now()) {}

TouchCalibratorController::~TouchCalibratorController() {
  touch_calibrator_widgets_.clear();
  already_mapped_display_ids_.clear();
  StopCalibrationAndResetParams();
}

void TouchCalibratorController::OnDidApplyDisplayChanges() {
  touch_calibrator_widgets_.clear();
  StopCalibrationAndResetParams();

  // Native touchscreen mapping state is not updated by
  // |StopCalibrationAndResetParam| since it would generally move on to the next
  // display afterwards. State must be reset in this case since display
  // configuration has changed and the current mapping instantiation is no
  // longer valid.
  if (state_ == CalibrationState::kNativeCalibrationTouchscreenMapping) {
    already_mapped_display_ids_.clear();
    state_ = CalibrationState::kInactive;
  }
}

void TouchCalibratorController::StartCalibration(
    const display::Display& target_display,
    bool is_custom_calibration,
    TouchCalibrationCallback opt_callback) {
  if (state_ != CalibrationState::kNativeCalibrationTouchscreenMapping) {
    state_ = is_custom_calibration ? CalibrationState::kCustomCalibration
                                   : CalibrationState::kNativeCalibration;
  }

  if (opt_callback) {
    opt_callback_ = std::move(opt_callback);
  }

  target_display_ = target_display;

  // Clear all touch calibrator views used in any previous calibration.
  touch_calibrator_widgets_.clear();

  // Set the touch device id as invalid so it can be set during calibration.
  touch_device_id_ = ui::InputDevice::kInvalidId;

  // Populate |internal_touch_device_ids_| with the ids of touch devices that
  // are currently associated with the internal display and are of type
  // |ui::InputDeviceType::INPUT_DEVICE_INTERNAL|.
  InitInternalTouchDeviceIds(internal_touch_device_ids_);

  // If this is a native touch calibration, then initialize the UX for it.
  if (state_ == CalibrationState::kNativeCalibration ||
      state_ == CalibrationState::kNativeCalibrationTouchscreenMapping) {
    Shell::Get()->display_manager()->AddDisplayManagerObserver(this);

    // Reset the calibration data.
    touch_point_quad_.fill(std::make_pair(gfx::Point(0, 0), gfx::Point(0, 0)));

    std::vector<display::Display> displays =
        display::Screen::GetScreen()->GetAllDisplays();

    for (const display::Display& display : displays) {
      bool is_primary_view = display.id() == target_display_.id();
      touch_calibrator_widgets_[display.id()] = TouchCalibratorView::Create(
          display, is_primary_view,
          state_ == CalibrationState::kNativeCalibrationTouchscreenMapping);
    }
  }

  Shell::Get()->touch_transformer_controller()->SetForCalibration(true);

  // Add self as an event handler target.
  Shell::Get()->AddPreTargetHandler(this);
}

void TouchCalibratorController::StartNativeTouchscreenMappingExperience(
    TouchCalibrationCallback opt_callback) {
  state_ = CalibrationState::kNativeCalibrationTouchscreenMapping;
  already_mapped_display_ids_.clear();
  CalibrateNextDisplay();
  opt_callback_all_displays_ = std::move(opt_callback);
}

void TouchCalibratorController::CalibrateNextDisplay() {
  CHECK(state_ == CalibrationState::kNativeCalibrationTouchscreenMapping);

  // Find the next external display to calibrate that we did not already handle.
  const auto& active_displays =
      Shell::Get()->display_manager()->active_display_list();
  const display::Display* next_display_to_map = nullptr;
  for (const auto& display : active_displays) {
    if (display.IsInternal() ||
        base::Contains(already_mapped_display_ids_, display.id())) {
      continue;
    }

    next_display_to_map = &display;
    break;
  }

  if (!next_display_to_map) {
    state_ = CalibrationState::kInactive;
    already_mapped_display_ids_.clear();
    if (opt_callback_all_displays_) {
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(opt_callback_all_displays_),
                                    /*success=*/true));
    }

    for (const auto& it : touch_calibrator_widgets_) {
      if (auto* touch_calibrator_view = views::AsViewClass<TouchCalibratorView>(
              it.second->GetContentsView())) {
        touch_calibrator_view->SkipToFinalState();
      }
    }
    return;
  }

  already_mapped_display_ids_.emplace(next_display_to_map->id());
  StartCalibration(*next_display_to_map, /*is_custom_calibration=*/false,
                   base::DoNothing());
}

void TouchCalibratorController::StopCalibrationAndResetParams() {
  Shell::Get()->display_manager()->RemoveDisplayManagerObserver(this);

  Shell::Get()->touch_transformer_controller()->SetForCalibration(false);

  // Remove self as the event handler.
  Shell::Get()->RemovePreTargetHandler(this);

  // Transition all touch calibrator views to their final state for a graceful
  // exit if this is touch calibration with native UX.
  if (state_ == CalibrationState::kNativeCalibration ||
      state_ == CalibrationState::kNativeCalibrationTouchscreenMapping) {
    for (const auto& it : touch_calibrator_widgets_) {
      if (auto* touch_calibrator_view = views::AsViewClass<TouchCalibratorView>(
              it.second->GetContentsView())) {
        touch_calibrator_view->SkipToFinalState();
      }
    }
  }

  if (state_ != CalibrationState::kNativeCalibrationTouchscreenMapping) {
    state_ = CalibrationState::kInactive;
  }

  if (opt_callback_) {
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(opt_callback_), false /* failure */));
    opt_callback_.Reset();
  }
}

void TouchCalibratorController::CompleteCalibration(
    const CalibrationPointPairQuad& pairs,
    const gfx::Size& display_size) {
  bool did_find_touch_device = false;
  const std::vector<ui::TouchscreenDevice>& device_list =
      ui::DeviceDataManager::GetInstance()->GetTouchscreenDevices();
  ui::TouchscreenDevice target_device;
  for (const auto& device : device_list) {
    if (device.id == touch_device_id_) {
      target_device = device;
      did_find_touch_device = true;
      break;
    }
  }

  if (!did_find_touch_device) {
    VLOG(1) << "No touch device with id: " << touch_device_id_ << " found to "
            << "complete touch calibration for display with id: "
            << target_display_.id() << ". Storing it as a fallback";
  }

  if (opt_callback_) {
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(opt_callback_), true /* success */));
    opt_callback_.Reset();
  }
  StopCalibrationAndResetParams();

  const bool in_native_touchscreen_mapping =
      state_ == CalibrationState::kNativeCalibrationTouchscreenMapping;
  Shell::Get()->display_manager()->SetTouchCalibrationData(
      target_display_.id(), pairs, display_size, target_device,
      /*apply_spatial_calibration=*/!in_native_touchscreen_mapping);
  if (in_native_touchscreen_mapping) {
    CalibrateNextDisplay();
  }
}

bool TouchCalibratorController::IsCalibrating() const {
  return state_ != CalibrationState::kInactive;
}

// ui::EventHandler:
void TouchCalibratorController::OnKeyEvent(ui::KeyEvent* key) {
  if (state_ != CalibrationState::kNativeCalibration &&
      state_ != CalibrationState::kNativeCalibrationTouchscreenMapping) {
    return;
  }
  // Detect ESC key press.
  if (key->type() == ui::EventType::kKeyPressed &&
      key->key_code() == ui::VKEY_ESCAPE) {
    StopCalibrationAndResetParams();
    if (state_ == CalibrationState::kNativeCalibrationTouchscreenMapping) {
      CalibrateNextDisplay();
    }
  }

  key->StopPropagation();
}

void TouchCalibratorController::OnTouchEvent(ui::TouchEvent* touch) {
  if (!IsCalibrating())
    return;
  if (touch->type() != ui::EventType::kTouchReleased) {
    return;
  }
  if (base::Time::Now() - last_touch_timestamp_ < kTouchIntervalThreshold)
    return;
  last_touch_timestamp_ = base::Time::Now();

  // If the touch event originated from a touch device that is associated with
  // the internal display, then ignore it.
  if (internal_touch_device_ids_.count(touch->source_device_id())) {
    return;
  }

  if (touch_device_id_ == ui::InputDevice::kInvalidId) {
    touch_device_id_ = touch->source_device_id();
    event_transformer_ = CalculateEventTransformer(touch_device_id_);
  }

  // If this is a custom touch calibration, then everything else is managed
  // by the application responsible for the custom calibration UX.
  if (state_ == CalibrationState::kCustomCalibration) {
    return;
  }
  touch->StopPropagation();

  TouchCalibratorView* target_screen_calibration_view =
      views::AsViewClass<TouchCalibratorView>(
          touch_calibrator_widgets_[target_display_.id()]->GetContentsView());
  CHECK(target_screen_calibration_view);

  // If this is the final state, then store all calibration data and stop
  // calibration.
  if (state_ ==
          TouchCalibratorController::CalibrationState::kNativeCalibration &&
      target_screen_calibration_view->state() ==
          TouchCalibratorView::CALIBRATION_COMPLETE) {
    gfx::RectF calibration_bounds =
        Shell::Get()
            ->window_tree_host_manager()
            ->GetAshWindowTreeHostForDisplayId(target_display_.id())
            ->AsWindowTreeHost()
            ->GetRootTransform()
            .MapRect(
                gfx::RectF(target_screen_calibration_view->GetLocalBounds()));
    CompleteCalibration(touch_point_quad_,
                        gfx::ToRoundedSize(calibration_bounds.size()));
    return;
  }

  int state_index;
  // Maps the state to an integer value. Assigns a non negative integral value
  // for a state in which the user can interact with the the interface.
  switch (target_screen_calibration_view->state()) {
    case TouchCalibratorView::DISPLAY_POINT_1:
      state_index = 0;
      break;
    case TouchCalibratorView::DISPLAY_POINT_2:
      state_index = 1;
      break;
    case TouchCalibratorView::DISPLAY_POINT_3:
      state_index = 2;
      break;
    case TouchCalibratorView::DISPLAY_POINT_4:
      state_index = 3;
      break;
    default:
      // Return early if the interface is in a state that does not allow user
      // interaction.
      return;
  }

  // Store touch point corresponding to its display point.
  gfx::Point display_point;
  if (target_screen_calibration_view->GetDisplayPointLocation(&display_point)) {
    // If the screen has a root transform applied, the display point does not
    // correctly map to the touch point. This is specially evident if the
    // display is rotated or a device scale factor is applied. The display point
    // needs to have the root transform applied as well to correctly pair it
    // with the touch point.
    display_point = Shell::Get()
                        ->window_tree_host_manager()
                        ->GetAshWindowTreeHostForDisplayId(target_display_.id())
                        ->AsWindowTreeHost()
                        ->GetRootTransform()
                        .MapPoint(display_point);

    // Why do we need this? To understand this we need to know the life of an
    // event location. The event location undergoes the following
    // transformations along its path from the device to the event handler that
    // is this class.
    //
    // Touch Device -> EventFactoryEvdev -> DrmWindowHost
    //     -> WindowEventDispatcher -> EventHandler(this)
    //
    //  - The touch device dispatches the raw device event location. Lets assume
    //    this is (x, y).
    //  - The EventFactoryEvdev applies a touch transform that includes the
    //    calibration information as well as an offset of the native bounds
    //    of the display. This effectively converts the coordinates of the event
    //    from the raw device event location to the native screen coordinates.
    //    It gets the offset information from DrmWindowHost via the
    //    ManagedDisplayInfo class. If the offset of the PlatformWindow is (A,B)
    //    then the event location after this stage would be (x + A, y + B).
    //  - The DrmWindowHost removes the offset from the event location so that
    //    the location becomes relative to the platform window's origin. In
    //    Chrome OS it so happens that each display is its own platform window.
    //    So an offset equal to the display's origin in screen space is
    //    subtracted from the event location. This effectively undoes the
    //    previous step's transformation. Thus the event location after this
    //    step is (x, y) again.
    //  - WindowEventDispatcher applies an inverse root transform on the event
    //    location. This means that if the display is rotated or has a device
    //    scale factor, then those transformation are also applied to the event
    //    location. It effectively converts the coordinates from platform window
    //    coordinates to the aura's root window coordinates. The display in
    //    context here is the display that is associated with the touch device
    //    from which the event originated from.
    //
    // Up until the output of DrmWindowHost, everything is as expected. But
    // WindowEventDispatcher applies an inverse root transform which modifies
    // the raw event location that we wanted. Moreover, it modifies the raw
    // event location using the root transform of the display that the touch
    // device was previously associated with. To solve this, we need to undo the
    // changes made to the event location by WindowEventDispatcher. This is what
    // is achieved by |event_transformer_|.
    gfx::PointF event_location_f =
        event_transformer_.MapPoint(touch->location_f());

    touch_point_quad_[state_index] =
        std::make_pair(display_point, gfx::ToRoundedPoint(event_location_f));
  } else {
    // TODO(malaykeshav): Display some kind of error for the user.
    NOTREACHED() << "Touch calibration failed. Could not retrieve location for"
                    " display point. Retry calibration.";
  }

  // For calibrating all displays, skip the final state of showing a
  // "Calibration complete" screen.
  if (state_ == TouchCalibratorController::CalibrationState::
                    kNativeCalibrationTouchscreenMapping &&
      target_screen_calibration_view->state() ==
          TouchCalibratorView::DISPLAY_POINT_4) {
    gfx::RectF calibration_bounds =
        Shell::Get()
            ->window_tree_host_manager()
            ->GetAshWindowTreeHostForDisplayId(target_display_.id())
            ->AsWindowTreeHost()
            ->GetRootTransform()
            .MapRect(
                gfx::RectF(target_screen_calibration_view->GetLocalBounds()));
    CompleteCalibration(touch_point_quad_,
                        gfx::ToRoundedSize(calibration_bounds.size()));
    return;
  }

  target_screen_calibration_view->AdvanceToNextState();
}

}  // namespace ash