chromium/ash/display/cros_display_config.cc

// Copyright 2018 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/cros_display_config.h"

#include <optional>
#include <sstream>
#include <utility>

#include "ash/constants/ash_features.h"
#include "ash/display/display_alignment_controller.h"
#include "ash/display/display_configuration_controller.h"
#include "ash/display/display_highlight_controller.h"
#include "ash/display/display_prefs.h"
#include "ash/display/overscan_calibrator.h"
#include "ash/display/resolution_notification_controller.h"
#include "ash/display/screen_orientation_controller.h"
#include "ash/display/touch_calibrator_controller.h"
#include "ash/display/window_tree_host_manager.h"
#include "ash/public/cpp/tablet_mode_observer.h"
#include "ash/shell.h"
#include "ash/touch/ash_touch_transform_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/strings/string_number_conversions.h"
#include "components/device_event_log/device_event_log.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "mojo/public/cpp/bindings/remote_set.h"
#include "ui/display/display.h"
#include "ui/display/display_layout.h"
#include "ui/display/display_layout_builder.h"
#include "ui/display/display_observer.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/manager/util/display_manager_util.h"
#include "ui/display/screen.h"

namespace ash {

namespace {

// Maximum allowed bounds origin absolute value.
constexpr int kMaxBoundsOrigin = 200 * 1000;

// This is the default range of display width in logical pixels allowed after
// applying display zoom.
constexpr int kDefaultMaxZoomWidth = 4096;
constexpr int kDefaultMinZoomWidth = 640;

display::DisplayManager* GetDisplayManager() {
  return Shell::Get()->display_manager();
}

int64_t GetDisplayId(const std::string& display_id_str) {
  int64_t display_id;
  if (!base::StringToInt64(display_id_str, &display_id)) {
    display_id = display::kInvalidDisplayId;
  }
  return display_id;
}

// Gets the display with the provided string id.
display::Display GetDisplay(const std::string& display_id_str) {
  int64_t display_id = GetDisplayId(display_id_str);
  if (display_id == display::kInvalidDisplayId) {
    return display::Display();
  }
  display::DisplayManager* display_manager = GetDisplayManager();
  if (display_manager->IsInUnifiedMode() &&
      display_id != display::kUnifiedDisplayId) {
    // In unified desktop mode, the mirroring displays, which constitue the
    // combined unified display, are contained in the software mirroring
    // displays list in the display manager.
    // The active list of displays only contains a single unified display entry
    // with id |kUnifiedDisplayId|.
    return display_manager->GetMirroringDisplayById(display_id);
  }
  return display_manager->GetDisplayForId(display_id);
}

crosapi::mojom::DisplayLayoutPosition GetMojomDisplayLayoutPosition(
    display::DisplayPlacement::Position position) {
  switch (position) {
    case display::DisplayPlacement::TOP:
      return crosapi::mojom::DisplayLayoutPosition::kTop;
    case display::DisplayPlacement::RIGHT:
      return crosapi::mojom::DisplayLayoutPosition::kRight;
    case display::DisplayPlacement::BOTTOM:
      return crosapi::mojom::DisplayLayoutPosition::kBottom;
    case display::DisplayPlacement::LEFT:
      return crosapi::mojom::DisplayLayoutPosition::kLeft;
  }
  NOTREACHED();
}

display::DisplayPlacement::Position GetDisplayPlacementPosition(
    crosapi::mojom::DisplayLayoutPosition position) {
  switch (position) {
    case crosapi::mojom::DisplayLayoutPosition::kTop:
      return display::DisplayPlacement::TOP;
    case crosapi::mojom::DisplayLayoutPosition::kRight:
      return display::DisplayPlacement::RIGHT;
    case crosapi::mojom::DisplayLayoutPosition::kBottom:
      return display::DisplayPlacement::BOTTOM;
    case crosapi::mojom::DisplayLayoutPosition::kLeft:
      return display::DisplayPlacement::LEFT;
  }
  NOTREACHED();
}

std::vector<crosapi::mojom::DisplayLayoutPtr> GetDisplayLayouts() {
  auto layouts = std::vector<crosapi::mojom::DisplayLayoutPtr>();
  display::Screen* screen = display::Screen::GetScreen();
  const std::vector<display::Display>& displays = screen->GetAllDisplays();
  display::DisplayManager* display_manager = GetDisplayManager();
  for (const display::Display& display : displays) {
    const display::DisplayPlacement placement =
        display_manager->GetCurrentResolvedDisplayLayout().FindPlacementById(
            display.id());
    if (placement.display_id == display::kInvalidDisplayId) {
      continue;
    }
    auto layout = crosapi::mojom::DisplayLayout::New();
    layout->id = base::NumberToString(placement.display_id);
    layout->parent_id = base::NumberToString(placement.parent_display_id);
    layout->position = GetMojomDisplayLayoutPosition(placement.position);
    layout->offset = placement.offset;
    layouts.emplace_back(std::move(layout));
  }
  return layouts;
}

std::vector<crosapi::mojom::DisplayLayoutPtr> GetDisplayUnifiedLayouts() {
  auto layouts = std::vector<crosapi::mojom::DisplayLayoutPtr>();
  display::DisplayManager* display_manager = GetDisplayManager();

  const display::UnifiedDesktopLayoutMatrix& matrix =
      display_manager->current_unified_desktop_matrix();
  for (size_t row_index = 0; row_index < matrix.size(); ++row_index) {
    const auto& row = matrix[row_index];
    for (size_t column_index = 0; column_index < row.size(); ++column_index) {
      if (column_index == 0 && row_index == 0) {
        // No placement for the primary display.
        continue;
      }
      auto layout = crosapi::mojom::DisplayLayout::New();
      const int64_t display_id = row[column_index];
      // Parent display is either the one in the above row, or the one on the
      // left in the same row.
      const int64_t parent_id = column_index == 0
                                    ? matrix[row_index - 1][column_index]
                                    : row[column_index - 1];
      layout->id = base::NumberToString(display_id);
      layout->parent_id = base::NumberToString(parent_id);
      layout->position = column_index == 0
                             ? crosapi::mojom::DisplayLayoutPosition::kBottom
                             : crosapi::mojom::DisplayLayoutPosition::kRight;
      layout->offset = 0;
      layouts.emplace_back(std::move(layout));
    }
  }
  return layouts;
}

crosapi::mojom::DisplayConfigResult SetDisplayLayoutMode(
    const crosapi::mojom::DisplayLayoutInfo& info) {
  display::DisplayManager* display_manager = GetDisplayManager();
  if (display_manager->num_connected_displays() < 2) {
    return crosapi::mojom::DisplayConfigResult::kSingleDisplayError;
  }

  if (info.layout_mode == crosapi::mojom::DisplayLayoutMode::kNormal) {
    display_manager->SetDefaultMultiDisplayModeForCurrentDisplays(
        display::DisplayManager::EXTENDED);
    display_manager->SetMirrorMode(display::MirrorMode::kOff, std::nullopt);
    return crosapi::mojom::DisplayConfigResult::kSuccess;
  }

  if (info.layout_mode == crosapi::mojom::DisplayLayoutMode::kUnified) {
    if (!display_manager->unified_desktop_enabled()) {
      return crosapi::mojom::DisplayConfigResult::kUnifiedNotEnabledError;
    }
    display_manager->SetDefaultMultiDisplayModeForCurrentDisplays(
        display::DisplayManager::UNIFIED);
    display_manager->SetMirrorMode(display::MirrorMode::kOff, std::nullopt);
    return crosapi::mojom::DisplayConfigResult::kSuccess;
  }

  DCHECK(info.layout_mode == crosapi::mojom::DisplayLayoutMode::kMirrored);

  // 'Normal' mirror mode.
  if (!info.mirror_source_id) {
    display_manager->SetMirrorMode(display::MirrorMode::kNormal, std::nullopt);
    return crosapi::mojom::DisplayConfigResult::kSuccess;
  }

  // 'Mixed' mirror mode.
  display::Display source = GetDisplay(*info.mirror_source_id);
  if (source.id() == display::kInvalidDisplayId) {
    return crosapi::mojom::DisplayConfigResult::kMirrorModeSourceIdError;
  }
  display::DisplayIdList destination_ids;
  if (info.mirror_destination_ids) {
    for (const std::string& id_str : *info.mirror_destination_ids) {
      int64_t destination_id = GetDisplayId(id_str);
      if (destination_id == display::kInvalidDisplayId) {
        return crosapi::mojom::DisplayConfigResult::kMirrorModeDestIdError;
      }
      destination_ids.emplace_back(destination_id);
    }
  } else {
    const std::vector<display::Display>& displays =
        display::Screen::GetScreen()->GetAllDisplays();
    for (const display::Display& display : displays) {
      destination_ids.emplace_back(display.id());
    }
  }
  std::optional<display::MixedMirrorModeParams> mixed_params(
      std::in_place, source.id(), destination_ids);
  const display::MixedMirrorModeParamsErrors error_type =
      display::ValidateParamsForMixedMirrorMode(
          display_manager->GetConnectedDisplayIdList(), *mixed_params);
  switch (error_type) {
    case display::MixedMirrorModeParamsErrors::kErrorSingleDisplay:
      return crosapi::mojom::DisplayConfigResult::kSingleDisplayError;
    case display::MixedMirrorModeParamsErrors::kErrorSourceIdNotFound:
      return crosapi::mojom::DisplayConfigResult::kMirrorModeSourceIdError;
    case display::MixedMirrorModeParamsErrors::kErrorDestinationIdsEmpty:
    case display::MixedMirrorModeParamsErrors::kErrorDestinationIdNotFound:
    case display::MixedMirrorModeParamsErrors::kErrorDuplicateId:
      return crosapi::mojom::DisplayConfigResult::kMirrorModeDestIdError;
    case display::MixedMirrorModeParamsErrors::kSuccess:
      break;
  }
  display_manager->SetMirrorMode(display::MirrorMode::kMixed, mixed_params);
  return crosapi::mojom::DisplayConfigResult::kSuccess;
}

crosapi::mojom::DisplayModePtr GetDisplayMode(
    const display::ManagedDisplayInfo& display_info,
    const display::ManagedDisplayMode& display_mode) {
  auto result = crosapi::mojom::DisplayMode::New();
  gfx::Size size_dip = display_mode.GetSizeInDIP();
  result->size = size_dip;
  result->size_in_native_pixels = display_mode.size();
  result->device_scale_factor = display_mode.device_scale_factor();
  result->refresh_rate = display_mode.refresh_rate();
  result->is_native = display_mode.native();
  result->is_interlaced = display_mode.is_interlaced();
  return result;
}

display::Display::Rotation DisplayRotationFromRotationOptions(
    crosapi::mojom::DisplayRotationOptions option) {
  switch (option) {
    case crosapi::mojom::DisplayRotationOptions::kAutoRotate:
      return display::Display::ROTATE_0;

    case crosapi::mojom::DisplayRotationOptions::kZeroDegrees:
      return display::Display::ROTATE_0;

    case crosapi::mojom::DisplayRotationOptions::k90Degrees:
      return display::Display::ROTATE_90;

    case crosapi::mojom::DisplayRotationOptions::k180Degrees:
      return display::Display::ROTATE_180;

    case crosapi::mojom::DisplayRotationOptions::k270Degrees:
      return display::Display::ROTATE_270;
  }
}

crosapi::mojom::DisplayRotationOptions RotationOptionsFromDisplayRotation(
    display::Display::Rotation rotation,
    bool is_internal) {
  auto* screen_orientation_controller =
      Shell::Get()->screen_orientation_controller();
  const bool is_auto_rotation_allowed =
      screen_orientation_controller->IsAutoRotationAllowed();
  const bool is_auto_rotate_enabled =
      !screen_orientation_controller->user_rotation_locked();
  if (is_auto_rotation_allowed && is_auto_rotate_enabled && is_internal) {
    return crosapi::mojom::DisplayRotationOptions::kAutoRotate;
  }

  switch (rotation) {
    case display::Display::ROTATE_0:
      return crosapi::mojom::DisplayRotationOptions::kZeroDegrees;

    case display::Display::ROTATE_90:
      return crosapi::mojom::DisplayRotationOptions::k90Degrees;

    case display::Display::ROTATE_180:
      return crosapi::mojom::DisplayRotationOptions::k180Degrees;

    case display::Display::ROTATE_270:
      return crosapi::mojom::DisplayRotationOptions::k270Degrees;
  }
}

crosapi::mojom::DisplayUnitInfoPtr GetDisplayUnitInfo(
    const display::Display& display,
    int64_t primary_id) {
  display::DisplayManager* display_manager = GetDisplayManager();
  const display::ManagedDisplayInfo& display_info =
      display_manager->GetDisplayInfo(display.id());

  auto info = crosapi::mojom::DisplayUnitInfo::New();
  info->id = base::NumberToString(display.id());
  info->name = display_manager->GetDisplayNameForId(display.id());

  if (!display_info.manufacturer_id().empty() ||
      !display_info.product_id().empty() ||
      (display_info.year_of_manufacture() !=
       display::kInvalidYearOfManufacture)) {
    info->edid = crosapi::mojom::Edid::New();
    info->edid->manufacturer_id = display_info.manufacturer_id();
    info->edid->product_id = display_info.product_id();
    info->edid->year_of_manufacture = display_info.year_of_manufacture();
  }

  info->is_primary = display.id() == primary_id;
  info->is_internal = display.IsInternal();
  info->is_enabled = true;
  info->is_detected = display.detected();
  info->is_auto_rotation_allowed =
      Shell::Get()->screen_orientation_controller()->IsAutoRotationAllowed() &&
      display.IsInternal();
  const bool has_accelerometer_support =
      display.accelerometer_support() ==
      display::Display::AccelerometerSupport::AVAILABLE;
  info->has_touch_support =
      display.touch_support() == display::Display::TouchSupport::AVAILABLE;
  info->has_accelerometer_support = has_accelerometer_support;

  const float device_dpi = display_info.device_dpi();
  info->dpi_x = device_dpi * display.size().width() /
                display_info.bounds_in_native().width();
  info->dpi_y = device_dpi * display.size().height() /
                display_info.bounds_in_native().height();

  info->rotation_options = RotationOptionsFromDisplayRotation(
      display.rotation(), display.IsInternal());
  info->bounds = display.bounds();
  info->overscan = display_manager->GetOverscanInsets(display.id());
  info->work_area = display.work_area();

  int display_mode_index = 0;
  display::ManagedDisplayMode active_mode;
  bool has_active_mode = display_manager->GetActiveModeForDisplayId(
      display_info.id(), &active_mode);
  for (const display::ManagedDisplayMode& display_mode :
       display_info.display_modes()) {
    info->available_display_modes.emplace_back(
        GetDisplayMode(display_info, display_mode));
    if (has_active_mode && display_mode.IsEquivalent(active_mode)) {
      info->selected_display_mode_index = display_mode_index;
    }
    ++display_mode_index;
  }

  info->display_zoom_factor = display_info.zoom_factor();
  if (has_active_mode) {
    auto zoom_levels = display::GetDisplayZoomFactors(active_mode);
    info->available_display_zoom_factors.reserve(zoom_levels.size());
    info->available_display_zoom_factors.assign(zoom_levels.begin(),
                                                zoom_levels.end());
  } else {
    info->available_display_zoom_factors.push_back(display_info.zoom_factor());
  }

  return info;
}

// Validates that DisplayProperties are valid with the current DisplayManager
// configuration. Returns an error on failure.
crosapi::mojom::DisplayConfigResult ValidateDisplayProperties(
    const crosapi::mojom::DisplayConfigProperties& properties,
    const display::Display& display) {
  display::DisplayManager* display_manager = GetDisplayManager();

  const crosapi::mojom::DisplayConfigProperties* prop_ptr = &properties;
  auto dump_state = [display, prop_ptr]() -> std::string {
    std::stringstream ss;
    ss << "display={" << display.ToString() << "}";
    ss << ", config properties={";
    if (prop_ptr->overscan) {
      ss << "overscan=" << prop_ptr->overscan->ToString() << ", ";
    }
    if (prop_ptr->bounds_origin) {
      ss << "bounds_origin=" << prop_ptr->bounds_origin->ToString() << ", ";
    }
    ss << "zoom_factor=" << prop_ptr->display_zoom_factor;
    return ss.str() + "}";
  };

  int64_t id = display.id();
  if (id == display::kInvalidDisplayId) {
    DISPLAY_LOG(ERROR) << "Invalid display id:" << dump_state();
    return crosapi::mojom::DisplayConfigResult::kInvalidDisplayIdError;
  }

  // Overscan cannot be changed for the internal display, and should be at most
  // half of the screen size.
  if (properties.overscan) {
    if (display.IsInternal()) {
      DISPLAY_LOG(ERROR) << "Overscan is not supported on the internal display:"
                         << dump_state();
      return crosapi::mojom::DisplayConfigResult::
          kNotSupportedOnInternalDisplayError;
    }

    if (properties.overscan->left() < 0 || properties.overscan->top() < 0 ||
        properties.overscan->right() < 0 || properties.overscan->bottom() < 0) {
      DISPLAY_LOG(ERROR) << "Negative overscan:" << dump_state();
      return crosapi::mojom::DisplayConfigResult::kPropertyValueOutOfRangeError;
    }

    const gfx::Insets overscan = display_manager->GetOverscanInsets(id);
    int screen_width = display.bounds().width() + overscan.width();
    int screen_height = display.bounds().height() + overscan.height();

    if ((properties.overscan->left() + properties.overscan->right()) * 2 >
            screen_width ||
        (properties.overscan->top() + properties.overscan->bottom()) * 2 >
            screen_height) {
      DISPLAY_LOG(ERROR) << "Invalid Overscan: " << dump_state()
                         << ", overscan (" << properties.overscan->ToString()
                         << ") exceeds bounds (" << screen_width << "x"
                         << screen_height << ")";
      return crosapi::mojom::DisplayConfigResult::kPropertyValueOutOfRangeError;
    }
  }

  // The bounds cannot be changed for the primary display and should be inside
  // a reasonable bounds.
  if (properties.bounds_origin) {
    const display::Display& primary =
        display::Screen::GetScreen()->GetPrimaryDisplay();
    if (id == primary.id() || properties.set_primary) {
      LOG(ERROR) << "Not Supported on Internal Display:" << dump_state();
      return crosapi::mojom::DisplayConfigResult::
          kNotSupportedOnInternalDisplayError;
    }
    if (properties.bounds_origin->x() > kMaxBoundsOrigin ||
        properties.bounds_origin->x() < -kMaxBoundsOrigin ||
        properties.bounds_origin->y() > kMaxBoundsOrigin ||
        properties.bounds_origin->y() < -kMaxBoundsOrigin) {
      DISPLAY_LOG(ERROR) << "Bounds origin out of range:" << dump_state();
      return crosapi::mojom::DisplayConfigResult::kPropertyValueOutOfRangeError;
    }
  }

  // In Unified mode, the actual zoom factor will be picked by the system.
  if (properties.display_zoom_factor >
      0) {  // && !display_manager->IsInUnifiedMode()) {
    display::ManagedDisplayMode current_mode;
    if (!display_manager->GetActiveModeForDisplayId(id, &current_mode)) {
      DISPLAY_LOG(ERROR) << "No active mode for display:" << dump_state();
      return crosapi::mojom::DisplayConfigResult::kInvalidDisplayIdError;
    }
    // This check is added to limit the range of display zoom that can be
    // applied via the system display API. The said range is such that when a
    // display zoom is applied, the final logical width in pixels should lie
    // within the range of 640 pixels and 4096 pixels.
    const int landscape_width =
        std::max(current_mode.size().width(), current_mode.size().height());
    const int max_allowed_width =
        std::max(kDefaultMaxZoomWidth, landscape_width);
    const int min_allowed_width =
        std::min(kDefaultMinZoomWidth, landscape_width);
    int current_width = static_cast<float>(landscape_width) /
                        current_mode.device_scale_factor();
    if (current_width / properties.display_zoom_factor > max_allowed_width ||
        current_width / properties.display_zoom_factor < min_allowed_width) {
      DISPLAY_LOG(ERROR) << "Display zoom factor out of range:" << dump_state();
      return crosapi::mojom::DisplayConfigResult::kPropertyValueOutOfRangeError;
    }
  }

  return crosapi::mojom::DisplayConfigResult::kSuccess;
}

// Sets the display layout for the target display in reference to the primary
// display.
void SetDisplayLayoutFromBounds(const gfx::Rect& primary_display_bounds,
                                int64_t primary_display_id,
                                const gfx::Rect& target_display_bounds,
                                int64_t target_display_id) {
  display::DisplayPlacement placement(
      display::DisplayLayout::CreatePlacementForRectangles(
          primary_display_bounds, target_display_bounds));
  placement.display_id = target_display_id;
  placement.parent_display_id = primary_display_id;

  std::unique_ptr<display::DisplayLayout> layout(new display::DisplayLayout);
  layout->placement_list.push_back(placement);
  layout->primary_id = primary_display_id;

  Shell::Get()->display_configuration_controller()->SetDisplayLayout(
      std::move(layout));
}

// Attempts to set the display mode for display |id|.
crosapi::mojom::DisplayConfigResult SetDisplayMode(
    int64_t id,
    const crosapi::mojom::DisplayMode& display_mode,
    crosapi::mojom::DisplayConfigSource source) {
  display::DisplayManager* display_manager = GetDisplayManager();

  display::ManagedDisplayMode current_mode;
  if (!display_manager->GetActiveModeForDisplayId(id, &current_mode)) {
    return crosapi::mojom::DisplayConfigResult::kInvalidDisplayIdError;
  }

  display::ManagedDisplayMode new_mode(
      display_mode.size_in_native_pixels, display_mode.refresh_rate,
      display_mode.is_interlaced, display_mode.is_native,
      display_mode.device_scale_factor);

  if (!new_mode.IsEquivalent(current_mode)) {
    // For the internal display, the display mode will be applied directly.
    // Otherwise a confirm/revert notification will be prepared first, and the
    // display mode will be applied. If the user accepts the mode change by
    // dismissing the notification, MaybeStoreDisplayPrefs() will be called back
    // to persist the new preferences.
    if (!Shell::Get()
             ->resolution_notification_controller()
             ->PrepareNotificationAndSetDisplayMode(
                 id, current_mode, new_mode, source, base::BindOnce([]() {
                   Shell::Get()->display_prefs()->MaybeStoreDisplayPrefs();
                 }))) {
      return crosapi::mojom::DisplayConfigResult::kSetDisplayModeError;
    }
  }

  return crosapi::mojom::DisplayConfigResult::kSuccess;
}

display::TouchCalibrationData::CalibrationPointPair GetCalibrationPair(
    const crosapi::mojom::TouchCalibrationPair& pair) {
  return std::make_pair(pair.display_point, pair.touch_point);
}

}  // namespace

// -----------------------------------------------------------------------------
// CrosDisplayConfig::ObserverImpl:

// Observes display and tablet mode events, and notifies the
// CrosDisplayConfigObservers with OnDisplayConfigChanged() in response to those
// events.
class CrosDisplayConfig::ObserverImpl
    : public display::DisplayObserver,
      public TabletModeObserver,
      public ScreenOrientationController::Observer {
 public:
  ObserverImpl() {
    Shell::Get()->tablet_mode_controller()->AddObserver(this);
    Shell::Get()->screen_orientation_controller()->AddObserver(this);
  }

  ObserverImpl(const ObserverImpl&) = delete;
  ObserverImpl& operator=(const ObserverImpl&) = delete;

  ~ObserverImpl() override {
    Shell::Get()->screen_orientation_controller()->RemoveObserver(this);
    Shell::Get()->tablet_mode_controller()->RemoveObserver(this);
  }

  void AddObserver(
      mojo::PendingAssociatedRemote<crosapi::mojom::CrosDisplayConfigObserver>
          observer) {
    observers_.Add(
        mojo::AssociatedRemote<crosapi::mojom::CrosDisplayConfigObserver>(
            std::move(observer)));
  }

  // display::DisplayObserver:
  void OnDisplayAdded(const display::Display& new_display) override {
    NotifyObserversDisplayConfigChanged();
  }

  void OnDisplaysRemoved(const display::Displays& removed_displays) override {
    NotifyObserversDisplayConfigChanged();
  }

  void OnDisplayMetricsChanged(const display::Display& display,
                               uint32_t metrics) override {
    NotifyObserversDisplayConfigChanged();
  }

  // TabletModeObserver:
  void OnTabletPhysicalStateChanged() override {
    NotifyObserversDisplayConfigChanged();
  }

  // ScreenOrientationController::Observer:
  void OnUserRotationLockChanged() override {
    NotifyObserversDisplayConfigChanged();
  }

 private:
  void NotifyObserversDisplayConfigChanged() {
    for (auto& observer : observers_) {
      observer->OnDisplayConfigChanged();
    }
  }

  mojo::AssociatedRemoteSet<crosapi::mojom::CrosDisplayConfigObserver>
      observers_;
  display::ScopedDisplayObserver display_observer_{this};
};

// -----------------------------------------------------------------------------
// CrosDisplayConfig:

CrosDisplayConfig::CrosDisplayConfig()
    : observer_impl_(std::make_unique<ObserverImpl>()) {}

CrosDisplayConfig::~CrosDisplayConfig() = default;

void CrosDisplayConfig::BindReceiver(
    mojo::PendingReceiver<crosapi::mojom::CrosDisplayConfigController>
        receiver) {
  receivers_.Add(this, std::move(receiver));
}

void CrosDisplayConfig::AddObserver(
    mojo::PendingAssociatedRemote<crosapi::mojom::CrosDisplayConfigObserver>
        observer) {
  observer_impl_->AddObserver(std::move(observer));
}

void CrosDisplayConfig::GetDisplayLayoutInfo(
    GetDisplayLayoutInfoCallback callback) {
  display::DisplayManager* display_manager = GetDisplayManager();

  auto info = crosapi::mojom::DisplayLayoutInfo::New();
  if (display_manager->IsInUnifiedMode()) {
    info->layout_mode = crosapi::mojom::DisplayLayoutMode::kUnified;
  } else if (display_manager->IsInMirrorMode()) {
    info->layout_mode = crosapi::mojom::DisplayLayoutMode::kMirrored;
    info->mirror_source_id =
        base::NumberToString(display_manager->mirroring_source_id());
    info->mirror_destination_ids = std::vector<std::string>();
    for (int64_t id : display_manager->GetMirroringDestinationDisplayIdList()) {
      info->mirror_destination_ids->emplace_back(base::NumberToString(id));
    }
  } else {
    info->layout_mode = crosapi::mojom::DisplayLayoutMode::kNormal;
  }

  if (display_manager->IsInUnifiedMode()) {
    info->layouts = GetDisplayUnifiedLayouts();
  } else if (display_manager->num_connected_displays() > 1) {
    info->layouts = GetDisplayLayouts();
  }

  std::move(callback).Run(std::move(info));
}

crosapi::mojom::DisplayConfigResult SetDisplayLayouts(
    const std::vector<crosapi::mojom::DisplayLayoutPtr>& layouts) {
  display::DisplayManager* display_manager = GetDisplayManager();
  display::DisplayLayoutBuilder builder(
      display_manager->GetCurrentResolvedDisplayLayout());
  int64_t root_id = display::kInvalidDisplayId;
  std::set<int64_t> layout_ids;
  builder.ClearPlacements();
  for (const crosapi::mojom::DisplayLayoutPtr& layout_ptr : layouts) {
    const crosapi::mojom::DisplayLayout& layout = *layout_ptr;
    display::Display display = GetDisplay(layout.id);
    if (display.id() == display::kInvalidDisplayId) {
      DISPLAY_LOG(ERROR) << "Display layout has invalid id: " << layout.id;
      return crosapi::mojom::DisplayConfigResult::kInvalidDisplayIdError;
    }
    display::Display parent = GetDisplay(layout.parent_id);
    if (parent.id() == display::kInvalidDisplayId) {
      if (root_id != display::kInvalidDisplayId) {
        DISPLAY_LOG(ERROR) << "Display layout has invalid parent: "
                           << layout.parent_id;
        return crosapi::mojom::DisplayConfigResult::kInvalidDisplayLayoutError;
      }
      root_id = display.id();
      continue;  // No placement for root (primary) display.
    }
    layout_ids.insert(display.id());
    display::DisplayPlacement::Position position =
        GetDisplayPlacementPosition(layout.position);
    builder.AddDisplayPlacement(display.id(), parent.id(), position,
                                layout.offset);
  }

  const display::DisplayIdList display_ids =
      display_manager->GetConnectedDisplayIdList();
  std::unique_ptr<display::DisplayLayout> layout = builder.Build();
  if (display_manager->IsInUnifiedMode()) {
    if (root_id == display::kInvalidDisplayId) {
      // Look for a display with no layout info to use as the root.
      for (int64_t id : display_ids) {
        if (!base::Contains(layout_ids, id)) {
          root_id = id;
          break;
        }
      }
      if (root_id == display::kInvalidDisplayId) {
        DISPLAY_LOG(ERROR) << "Invalid unified layout: No root display id";
        return crosapi::mojom::DisplayConfigResult::kInvalidDisplayLayoutError;
      }
    }
    layout->primary_id = root_id;
    display::UnifiedDesktopLayoutMatrix matrix;
    if (!display::BuildUnifiedDesktopMatrix(display_ids, *layout, &matrix)) {
      DISPLAY_LOG(ERROR)
          << "Invalid unified layout: No proper conversion to a matrix";
      return crosapi::mojom::DisplayConfigResult::kInvalidDisplayLayoutError;
    }
    Shell::Get()
        ->display_configuration_controller()
        ->SetUnifiedDesktopLayoutMatrix(matrix);
  } else {
    if (!display::DisplayLayout::Validate(display_ids, *layout)) {
      // No need to log an error since `Validate` already logged what's wrong.
      return crosapi::mojom::DisplayConfigResult::kInvalidDisplayLayoutError;
    }
    Shell::Get()->display_configuration_controller()->SetDisplayLayout(
        std::move(layout));
  }
  return crosapi::mojom::DisplayConfigResult::kSuccess;
}

void CrosDisplayConfig::SetDisplayLayoutInfo(
    crosapi::mojom::DisplayLayoutInfoPtr info,
    SetDisplayLayoutInfoCallback callback) {
  crosapi::mojom::DisplayConfigResult result = SetDisplayLayoutMode(*info);
  if (result != crosapi::mojom::DisplayConfigResult::kSuccess) {
    std::move(callback).Run(result);
    return;
  }
  if (info->layouts) {
    result = SetDisplayLayouts(*info->layouts);
    if (result != crosapi::mojom::DisplayConfigResult::kSuccess) {
      std::move(callback).Run(result);
      return;
    }
  }
  std::move(callback).Run(crosapi::mojom::DisplayConfigResult::kSuccess);
}

void CrosDisplayConfig::GetDisplayUnitInfoList(
    bool single_unified,
    GetDisplayUnitInfoListCallback callback) {
  std::vector<crosapi::mojom::DisplayUnitInfoPtr> info_list;
  display::DisplayManager* display_manager = GetDisplayManager();

  std::vector<display::Display> displays;
  int64_t primary_id;
  if (!display_manager->IsInUnifiedMode()) {
    displays = display::Screen::GetScreen()->GetAllDisplays();
    primary_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
  } else if (single_unified) {
    for (size_t i = 0; i < display_manager->GetNumDisplays(); ++i) {
      displays.push_back(display_manager->GetDisplayAt(i));
    }
    primary_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
  } else {
    displays = display_manager->software_mirroring_display_list();
    primary_id = Shell::Get()
                     ->display_configuration_controller()
                     ->GetPrimaryMirroringDisplayForUnifiedDesktop()
                     .id();
  }

  for (const display::Display& display : displays) {
    info_list.emplace_back(GetDisplayUnitInfo(display, primary_id));
  }
  std::move(callback).Run(std::move(info_list));
}

void CrosDisplayConfig::SetDisplayProperties(
    const std::string& id,
    crosapi::mojom::DisplayConfigPropertiesPtr properties,
    crosapi::mojom::DisplayConfigSource source,
    SetDisplayPropertiesCallback callback) {
  const display::Display display = GetDisplay(id);
  crosapi::mojom::DisplayConfigResult result =
      ValidateDisplayProperties(*properties, display);
  if (result != crosapi::mojom::DisplayConfigResult::kSuccess) {
    std::move(callback).Run(result);
    return;
  }

  display::DisplayManager* display_manager = GetDisplayManager();
  DisplayConfigurationController* display_configuration_controller =
      Shell::Get()->display_configuration_controller();
  const display::Display& primary =
      display::Screen::GetScreen()->GetPrimaryDisplay();

  if (properties->set_primary && display.id() != primary.id()) {
    display_configuration_controller->SetPrimaryDisplayId(
        display.id(), false /* don't throttle */);
  }

  if (properties->overscan) {
    display_manager->SetOverscanInsets(display.id(), *properties->overscan);
  }

  if (properties->rotation) {
    const crosapi::mojom::DisplayRotationOptions rotation_options =
        properties->rotation->rotation;
    auto* screen_orientation_controller =
        Shell::Get()->screen_orientation_controller();
    const bool is_auto_rotation_allowed =
        screen_orientation_controller->IsAutoRotationAllowed();
    const bool auto_rotate_requested =
        rotation_options == crosapi::mojom::DisplayRotationOptions::kAutoRotate;

    display::Display::Rotation rotation =
        DisplayRotationFromRotationOptions(properties->rotation->rotation);
    if (is_auto_rotation_allowed && display.IsInternal()) {
      if (auto_rotate_requested) {
        if (screen_orientation_controller->user_rotation_locked()) {
          screen_orientation_controller->ToggleUserRotationLock();
        }
      } else {
        screen_orientation_controller->SetLockToRotation(rotation);
      }
    } else {
      display_configuration_controller->SetDisplayRotation(
          display.id(), rotation, display::Display::RotationSource::USER);
    }
  }

  if (properties->bounds_origin &&
      *properties->bounds_origin != display.bounds().origin()) {
    gfx::Rect display_bounds = display.bounds();
    display_bounds.Offset(
        properties->bounds_origin->x() - display.bounds().x(),
        properties->bounds_origin->y() - display.bounds().y());
    SetDisplayLayoutFromBounds(primary.bounds(), primary.id(), display_bounds,
                               display.id());
  }

  if (properties->display_zoom_factor > 0) {
    display_manager->UpdateZoomFactor(display.id(),
                                      properties->display_zoom_factor);
  }

  // Set the display mode. Note: if this returns an error, other properties
  // will have already been applied. TODO(stevenjb): Validate the display mode
  // before applying any properties.
  if (properties->display_mode) {
    result = SetDisplayMode(display.id(), *properties->display_mode, source);
    if (result != crosapi::mojom::DisplayConfigResult::kSuccess) {
      std::move(callback).Run(result);
      return;
    }
  }

  std::move(callback).Run(crosapi::mojom::DisplayConfigResult::kSuccess);
}

void CrosDisplayConfig::SetUnifiedDesktopEnabled(bool enabled) {
  GetDisplayManager()->SetUnifiedDesktopEnabled(enabled);
}

void CrosDisplayConfig::OverscanCalibration(
    const std::string& display_id,
    crosapi::mojom::DisplayConfigOperation op,
    const std::optional<gfx::Insets>& delta,
    OverscanCalibrationCallback callback) {
  display::Display display = GetDisplay(display_id);
  if (display.id() == display::kInvalidDisplayId) {
    std::move(callback).Run(
        crosapi::mojom::DisplayConfigResult::kInvalidDisplayIdError);
    return;
  }

  OverscanCalibrator* calibrator = GetOverscanCalibrator(display_id);
  if (!calibrator && op != crosapi::mojom::DisplayConfigOperation::kStart) {
    DISPLAY_LOG(ERROR) << "Calibrator does not exist for op=" << op;
    std::move(callback).Run(
        crosapi::mojom::DisplayConfigResult::kCalibrationNotAvailableError);
    return;
  }
  switch (op) {
    case crosapi::mojom::DisplayConfigOperation::kStart: {
      DVLOG(1) << "OverscanCalibrationStart: " << display_id;
      gfx::Insets insets =
          Shell::Get()->window_tree_host_manager()->GetOverscanInsets(
              display.id());
      if (calibrator) {
        DVLOG(1) << "Replacing existing calibrator for id: " << display_id;
      }
      overscan_calibrators_[display_id] =
          std::make_unique<OverscanCalibrator>(display, insets);
      break;
    }
    case crosapi::mojom::DisplayConfigOperation::kAdjust:
      DVLOG(1) << "OverscanCalibrationAdjust: " << display_id;
      if (!delta) {
        DISPLAY_LOG(ERROR) << "Delta not provided for for adjust: "
                           << display_id;
        std::move(callback).Run(
            crosapi::mojom::DisplayConfigResult::kCalibrationFailedError);
        return;
      }
      calibrator->UpdateInsets(calibrator->insets() + *delta);
      break;
    case crosapi::mojom::DisplayConfigOperation::kReset:
      DVLOG(1) << "OverscanCalibrationReset: " << display_id;
      calibrator->Reset();
      break;
    case crosapi::mojom::DisplayConfigOperation::kComplete:
      DVLOG(1) << "OverscanCalibrationComplete: " << display_id;
      calibrator->Commit();
      overscan_calibrators_[display_id].reset();
      break;
    case crosapi::mojom::DisplayConfigOperation::kShowNative:
      DISPLAY_LOG(ERROR) << "Operation not supported: " << op;
      std::move(callback).Run(
          crosapi::mojom::DisplayConfigResult::kInvalidOperationError);
      return;
    case crosapi::mojom::DisplayConfigOperation::kShowNativeMappingDisplays:
      DISPLAY_LOG(ERROR) << "Operation not supported: " << op;
      std::move(callback).Run(
          crosapi::mojom::DisplayConfigResult::kInvalidOperationError);
      return;
  }
  std::move(callback).Run(crosapi::mojom::DisplayConfigResult::kSuccess);
}

void CrosDisplayConfig::TouchCalibration(
    const std::string& display_id,
    crosapi::mojom::DisplayConfigOperation op,
    crosapi::mojom::TouchCalibrationPtr calibration,
    TouchCalibrationCallback callback) {
  // For native touch display mapping.
  if (op ==
      crosapi::mojom::DisplayConfigOperation::kShowNativeMappingDisplays) {
    if (touch_calibrator_ && touch_calibrator_->IsCalibrating()) {
      DISPLAY_LOG(ERROR) << "Touch calibration already active.";
      std::move(callback).Run(
          crosapi::mojom::DisplayConfigResult::kCalibrationInProgressError);
      return;
    }
    if (!touch_calibrator_) {
      touch_calibrator_ = std::make_unique<TouchCalibratorController>();
    }
    // For native calibration, |callback| is not run until calibration
    // completes.
    touch_calibrator_->StartNativeTouchscreenMappingExperience(base::BindOnce(
        [](TouchCalibrationCallback callback, bool result) {
          std::move(callback).Run(
              result ? crosapi::mojom::DisplayConfigResult::kSuccess
                     : crosapi::mojom::DisplayConfigResult::
                           kCalibrationFailedError);
        },
        std::move(callback)));
    return;
  }

  display::Display display = GetDisplay(display_id);
  if (display.id() == display::kInvalidDisplayId) {
    std::move(callback).Run(
        crosapi::mojom::DisplayConfigResult::kInvalidDisplayIdError);
    return;
  }
  if (display.IsInternal()) {
    DISPLAY_LOG(ERROR) << "Internal display cannot be calibrated for touch: "
                       << display_id;
    std::move(callback).Run(
        crosapi::mojom::DisplayConfigResult::kCalibrationNotAvailableError);
    return;
  }
  if (!display::HasExternalTouchscreenDevice()) {
    DISPLAY_LOG(ERROR)
        << "Touch calibration called with no external touch screen device.";
    std::move(callback).Run(
        crosapi::mojom::DisplayConfigResult::kCalibrationNotAvailableError);
    return;
  }

  if (op == crosapi::mojom::DisplayConfigOperation::kStart ||
      op == crosapi::mojom::DisplayConfigOperation::kShowNative) {
    if (touch_calibrator_ && touch_calibrator_->IsCalibrating()) {
      DISPLAY_LOG(ERROR) << "Touch calibration already active.";
      std::move(callback).Run(
          crosapi::mojom::DisplayConfigResult::kCalibrationInProgressError);
      return;
    }
    if (!touch_calibrator_) {
      touch_calibrator_ = std::make_unique<TouchCalibratorController>();
    }
    if (op == crosapi::mojom::DisplayConfigOperation::kShowNative) {
      // For native calibration, |callback| is not run until calibration
      // completes.
      touch_calibrator_->StartCalibration(
          display, /*is_custom_calibration=*/false,
          base::BindOnce(
              [](TouchCalibrationCallback callback, bool result) {
                std::move(callback).Run(
                    result ? crosapi::mojom::DisplayConfigResult::kSuccess
                           : crosapi::mojom::DisplayConfigResult::
                                 kCalibrationFailedError);
              },
              std::move(callback)));
      return;
    }
    // For custom calibration, start calibration and run |callback| now.
    touch_calibrator_->StartCalibration(display, /*is_custom_calibration=*/true,
                                        base::OnceCallback<void(bool)>());
    std::move(callback).Run(crosapi::mojom::DisplayConfigResult::kSuccess);
    return;
  }

  if (op == crosapi::mojom::DisplayConfigOperation::kReset) {
    Shell::Get()->display_manager()->ClearTouchCalibrationData(display.id(),
                                                               std::nullopt);
    std::move(callback).Run(crosapi::mojom::DisplayConfigResult::kSuccess);
    return;
  }

  if (op != crosapi::mojom::DisplayConfigOperation::kComplete) {
    DISPLAY_LOG(ERROR) << "Unknown operation: " << op;
    std::move(callback).Run(
        crosapi::mojom::DisplayConfigResult::kCalibrationNotStartedError);
    return;
  }

  if (!touch_calibrator_) {
    DISPLAY_LOG(ERROR) << "Touch calibration not active.";
    std::move(callback).Run(
        crosapi::mojom::DisplayConfigResult::kCalibrationNotStartedError);
    return;
  }

  if (!calibration || calibration->pairs.size() != 4) {
    DISPLAY_LOG(ERROR) << "Touch calibration requires four calibration pairs.";
    std::move(callback).Run(
        crosapi::mojom::DisplayConfigResult::kCalibrationInvalidDataError);
    return;
  }

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

  display::TouchCalibrationData::CalibrationPointPairQuad calibration_points;
  calibration_points[0] = GetCalibrationPair(*calibration->pairs[0]);
  calibration_points[1] = GetCalibrationPair(*calibration->pairs[1]);
  calibration_points[2] = GetCalibrationPair(*calibration->pairs[2]);
  calibration_points[3] = GetCalibrationPair(*calibration->pairs[3]);

  gfx::Size bounds = calibration->bounds;
  for (auto& calibration_point : calibration_points) {
    // Coordinates for display and touch point cannot be negative.
    if (calibration_point.first.x() < 0 || calibration_point.first.y() < 0 ||
        calibration_point.second.x() < 0 || calibration_point.second.y() < 0) {
      DISPLAY_LOG(ERROR)
          << "Display points and touch points cannot have negative coordinates";
      touch_calibrator_->StopCalibrationAndResetParams();
      std::move(callback).Run(
          crosapi::mojom::DisplayConfigResult::kCalibrationInvalidDataError);
      return;
    }
    // Coordinates for display points cannot be greater than the screen
    // bounds.
    if (calibration_point.first.x() > bounds.width() ||
        calibration_point.first.y() > bounds.height()) {
      DISPLAY_LOG(ERROR)
          << "Display point coordinates cannot be more than size of the "
             "display.";
      touch_calibrator_->StopCalibrationAndResetParams();
      std::move(callback).Run(
          crosapi::mojom::DisplayConfigResult::kCalibrationInvalidDataError);
      return;
    }
  }

  touch_calibrator_->CompleteCalibration(calibration_points, bounds);
  std::move(callback).Run(crosapi::mojom::DisplayConfigResult::kSuccess);
}

OverscanCalibrator* CrosDisplayConfig::GetOverscanCalibrator(
    const std::string& id) {
  auto iter = overscan_calibrators_.find(id);
  return iter == overscan_calibrators_.end() ? nullptr : iter->second.get();
}

void CrosDisplayConfig::HighlightDisplay(int64_t display_id) {
  Shell::Get()->display_highlight_controller()->SetHighlightedDisplay(
      display_id);
}

void CrosDisplayConfig::DragDisplayDelta(int64_t display_id,
                                         int32_t delta_x,
                                         int32_t delta_y) {
  DCHECK(features::IsDisplayAlignmentAssistanceEnabled());
  Shell::Get()->display_alignment_controller()->DisplayDragged(
      display_id, delta_x, delta_y);
}

}  // namespace ash