chromium/components/exo/drag_drop_operation.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/exo/drag_drop_operation.h"

#include "ash/drag_drop/drag_drop_controller.h"
#include "base/barrier_closure.h"
#include "base/check.h"
#include "base/memory/raw_ptr.h"
#include "base/pickle.h"
#include "base/strings/string_split.h"
#include "base/task/sequenced_task_runner.h"
#include "components/exo/data_exchange_delegate.h"
#include "components/exo/data_offer.h"
#include "components/exo/data_source.h"
#include "components/exo/extended_drag_source.h"
#include "components/exo/seat.h"
#include "components/exo/shell_surface_base.h"
#include "components/exo/shell_surface_util.h"
#include "components/exo/surface.h"
#include "components/exo/surface_tree_host.h"
#include "components/viz/common/frame_sinks/copy_output_request.h"
#include "components/viz/common/frame_sinks/copy_output_result.h"
#include "ui/aura/client/drag_drop_client.h"
#include "ui/aura/window_tracker.h"
#include "ui/base/clipboard/file_info.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint_serializer.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/compositor/layer.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point_conversions.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/gfx/geometry/vector2d.h"
#include "url/gurl.h"

namespace exo {
namespace {

using ::ui::mojom::DragOperation;

uint32_t DndActionsToDragOperations(const base::flat_set<DndAction>& actions) {
  uint32_t dnd_operations = 0;
  for (const DndAction action : actions) {
    switch (action) {
      case DndAction::kNone:
        [[fallthrough]];
        // We don't support the ask action
      case DndAction::kAsk:
        break;
      case DndAction::kCopy:
        dnd_operations |= ui::DragDropTypes::DragOperation::DRAG_COPY;
        break;
      case DndAction::kMove:
        dnd_operations |= ui::DragDropTypes::DragOperation::DRAG_MOVE;
        break;
    }
  }
  return dnd_operations;
}

DndAction DragOperationsToPreferredDndAction(int op) {
  if (op & ui::DragDropTypes::DragOperation::DRAG_COPY)
    return DndAction::kCopy;

  if (op & ui::DragDropTypes::DragOperation::DRAG_MOVE)
    return DndAction::kMove;

  return DndAction::kNone;
}

DndAction DragOperationToDndAction(DragOperation op) {
  switch (op) {
    case DragOperation::kNone:
      return DndAction::kNone;
    case DragOperation::kMove:
      return DndAction::kMove;
    case DragOperation::kCopy:
      return DndAction::kCopy;
    case DragOperation::kLink:
      return DndAction::kAsk;
    default:
      NOTREACHED_IN_MIGRATION() << op;
      return DndAction::kNone;
  }
  NOTREACHED_IN_MIGRATION();
}

}  // namespace

// Internal representation of a drag icon surface. Used when a non-null surface
// is passed in wl_data_device::start_drag requests.
// TODO(crbug.com/40145458): Rework icon implementation to avoid frame copies.
class DragDropOperation::IconSurface final : public SurfaceTreeHost,
                                             public ScopedSurface {
 public:
  IconSurface(Surface* icon, DragDropOperation* operation)
      : SurfaceTreeHost("ExoDragIcon"),
        ScopedSurface(icon, operation),
        operation_(operation) {
    DCHECK(operation_);
    DCHECK(!icon->HasSurfaceDelegate());

    Surface* origin_surface = operation_->origin_->get();
    origin_surface->window()->AddChild(host_window());
    SetRootSurface(icon);
  }

  IconSurface(const IconSurface&) = delete;
  IconSurface& operator=(const IconSurface&) = delete;
  ~IconSurface() override = default;

 private:
  // SurfaceTreeHost:
  void OnSurfaceCommit() override {
    SurfaceTreeHost::OnSurfaceCommit();
    RequestCaptureIcon();
  }

  void RequestCaptureIcon() {
    SubmitCompositorFrame();

    std::unique_ptr<viz::CopyOutputRequest> request =
        std::make_unique<viz::CopyOutputRequest>(
            viz::CopyOutputRequest::ResultFormat::RGBA,
            viz::CopyOutputRequest::ResultDestination::kSystemMemory,
            base::BindOnce(&IconSurface::OnCaptured,
                           weak_ptr_factory_.GetWeakPtr()));
    request->set_result_task_runner(
        base::SequencedTaskRunner::GetCurrentDefault());

    host_window()->layer()->RequestCopyOfOutput(std::move(request));
  }

  void OnCaptured(std::unique_ptr<viz::CopyOutputResult> icon_result) {
    // An empty response means the request was deleted before it was completed.
    // If this happens, and no operation has yet finished, restart the capture.
    if (icon_result->IsEmpty()) {
      RequestCaptureIcon();
      return;
    }

    auto scoped_bitmap = icon_result->ScopedAccessSkBitmap();
    operation_->OnDragIconCaptured(scoped_bitmap.GetOutScopedBitmap());
  }

  const raw_ptr<DragDropOperation> operation_;
  base::WeakPtrFactory<IconSurface> weak_ptr_factory_{this};
};

base::WeakPtr<DragDropOperation> DragDropOperation::Create(
    DataExchangeDelegate* data_exchange_delegate,
    DataSource* source,
    Surface* origin,
    Surface* icon,
    const gfx::PointF& drag_start_point,
    ui::mojom::DragEventSource event_source) {
  auto* dnd_op = new DragDropOperation(data_exchange_delegate, source, origin,
                                       icon, drag_start_point, event_source);
  return dnd_op->weak_ptr_factory_.GetWeakPtr();
}

DragDropOperation::DragDropOperation(
    DataExchangeDelegate* data_exchange_delegate,
    DataSource* source,
    Surface* origin,
    Surface* icon,
    const gfx::PointF& drag_start_point,
    ui::mojom::DragEventSource event_source)
    : source_(std::make_unique<ScopedDataSource>(source, this)),
      origin_(std::make_unique<ScopedSurface>(origin, this)),
      drag_start_point_(drag_start_point),
      os_exchange_data_(std::make_unique<ui::OSExchangeData>()),
      event_source_(event_source) {
  aura::Window* root_window = origin_->get()->window()->GetRootWindow();
  DCHECK(root_window);
  drag_drop_controller_ = static_cast<ash::DragDropController*>(
      aura::client::GetDragDropClient(root_window));
  DCHECK(drag_drop_controller_);

  if (drag_drop_controller_->IsDragDropInProgress())
    drag_drop_controller_->DragCancel();

  drag_drop_controller_->AddObserver(this);

  ui::EndpointType endpoint_type =
      data_exchange_delegate->GetDataTransferEndpointType(
          origin_->get()->window());
  os_exchange_data_->SetSource(
      std::make_unique<ui::DataTransferEndpoint>(endpoint_type));

  extended_drag_source_ = ExtendedDragSource::Get();
  if (extended_drag_source_) {
    drag_drop_controller_->set_toplevel_window_drag_delegate(
        extended_drag_source_);
    extended_drag_source_->AddObserver(this);
  }

  int num_additional_callbacks = 0;

  // TODO(crbug.com/40061238): Remove this once the issue is fixed.
  std::string callbacks;

  // TODO(crbug.com/1298033): Move DTE retrieval into
  // DataSource::GetDataForPreferredMimeTypes()
  // Lacros sends additional metadata, in a custom MIME type, to sync drag
  // source metadata. Hence, the number of callbacks is incremented by one.
  if (endpoint_type == ui::EndpointType::kLacros) {
    callbacks += "lacros,";
    ++num_additional_callbacks;
  }

  // When the icon is present, we increment the number of callbacks so we can
  // wait for the icon to be captured as well.
  if (icon) {
    icon_ = std::make_unique<IconSurface>(icon, this);
    ++num_additional_callbacks;
    callbacks += "icon,";
  }

  auto start_op_callback =
      base::BindOnce(&DragDropOperation::ScheduleStartDragDropOperation,
                     weak_ptr_factory_.GetWeakPtr());

  // TODO(crbug.com/40061238): Remove these when the issue is fixed.
  start_drag_drop_timer_.Start(FROM_HERE, base::Seconds(2), this,
                               &DragDropOperation::DragDataReadTimeout);
  LOG(ERROR) << "Starting data read for drag operation: additonal callbacks:"
             << callbacks;

  counter_ =
      base::BarrierClosure(DataSource::kMaxDataTypes + num_additional_callbacks,
                           std::move(start_op_callback));

  // TODO(crbug.com/1298033): Move DTE retrieval into
  // DataSource::GetDataForPreferredMimeTypes()
  if (endpoint_type == ui::EndpointType::kLacros) {
    source->ReadDataTransferEndpoint(
        base::BindOnce(&DragDropOperation::OnDataTransferEndpointRead,
                       weak_ptr_factory_.GetWeakPtr()),
        counter_);
  }

  source->GetDataForPreferredMimeTypes(
      base::BindOnce(&DragDropOperation::OnTextRead,
                     weak_ptr_factory_.GetWeakPtr()),
      DataSource::ReadDataCallback(),
      base::BindOnce(&DragDropOperation::OnHTMLRead,
                     weak_ptr_factory_.GetWeakPtr()),
      DataSource::ReadDataCallback(),
      base::BindOnce(&DragDropOperation::OnFilenamesRead,
                     weak_ptr_factory_.GetWeakPtr(), data_exchange_delegate,
                     origin->window()),
      base::BindOnce(&DragDropOperation::OnFileContentsRead,
                     weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&DragDropOperation::OnWebCustomDataRead,
                     weak_ptr_factory_.GetWeakPtr()),
      counter_);
}

DragDropOperation::~DragDropOperation() {
  drag_drop_controller_->RemoveObserver(this);

  if (source_)
    source_->get()->Cancelled();

  if (drag_drop_controller_->IsDragDropInProgress() && started_) {
    drag_drop_controller_->DragCancel();
  }

  if (extended_drag_source_)
    ResetExtendedDragSource();
}

void DragDropOperation::AbortIfPending() {
  if (!started_) {
    delete this;
  }
}

void DragDropOperation::OnDataTransferEndpointRead(const std::string& mime_type,
                                                   std::u16string data) {
  DCHECK(os_exchange_data_);

  std::string utf8_json = base::UTF16ToUTF8(data);
  auto drag_source_dte = ui::ConvertJsonToDataTransferEndpoint(utf8_json);

  os_exchange_data_->SetSource(std::move(drag_source_dte));

  counter_.Run();
}

void DragDropOperation::OnTextRead(const std::string& mime_type,
                                   std::u16string data) {
  DCHECK(os_exchange_data_);
  os_exchange_data_->SetString(std::move(data));

  // Prefer to use the HTML MIME type if possible
  if (mime_type_.empty())
    mime_type_ = mime_type;
  counter_.Run();
}

void DragDropOperation::OnHTMLRead(const std::string& mime_type,
                                   std::u16string data) {
  DCHECK(os_exchange_data_);
  os_exchange_data_->SetHtml(std::move(data), GURL());
  mime_type_ = mime_type;
  counter_.Run();
}

void DragDropOperation::OnFilenamesRead(
    DataExchangeDelegate* data_exchange_delegate,
    aura::Window* source,
    const std::string& mime_type,
    const std::vector<uint8_t>& data) {
  DCHECK(os_exchange_data_);
  os_exchange_data_->SetFilenames(source_->get()->GetFilenames(
      data_exchange_delegate->GetDataTransferEndpointType(source), data));
  mime_type_ = mime_type;
  counter_.Run();
}

void DragDropOperation::OnFileContentsRead(const std::string& mime_type,
                                           const base::FilePath& filename,
                                           const std::vector<uint8_t>& data) {
  DCHECK(os_exchange_data_);
  os_exchange_data_->SetFileContents(filename,
                                     std::string(data.begin(), data.end()));
  mime_type_ = mime_type;
  counter_.Run();
}

void DragDropOperation::OnWebCustomDataRead(const std::string& mime_type,
                                            const std::vector<uint8_t>& data) {
  DCHECK(os_exchange_data_);
  base::Pickle pickle = base::Pickle::WithUnownedBuffer(data);
  os_exchange_data_->SetPickledData(
      ui::ClipboardFormatType::DataTransferCustomType(), pickle);
  mime_type_ = mime_type;
  counter_.Run();
}

void DragDropOperation::OnDragIconCaptured(const SkBitmap& icon_bitmap) {
  DCHECK(icon_);

  float scale_factor = origin_->get()->window()->layer()->device_scale_factor();
  gfx::ImageSkia icon_skia =
      gfx::ImageSkia::CreateFromBitmap(icon_bitmap, scale_factor);
  gfx::Vector2d icon_offset = -icon_->get()->GetBufferOffset();

  if (os_exchange_data_) {
    os_exchange_data_->provider().SetDragImage(icon_skia, icon_offset);
  } else {
    drag_drop_controller_->SetDragImage(icon_skia, icon_offset);
  }

  if (!captured_icon_) {
    captured_icon_ = true;
    counter_.Run();
  }
}

void DragDropOperation::ScheduleStartDragDropOperation() {
  start_drag_drop_timer_.Stop();

  // StartDragAndDrop uses a nested run loop. When restarting, we a) don't want
  // to interrupt the callers task for an arbitrary period of time and b) want
  // to let any nested run loops that are currently running to have a chance to
  // exit to avoid arbitrarily deep nesting. We can accomplish both of those
  // things by posting a new task to actually start the drag and drop operation.
  if (extended_drag_source_) {
    ShellSurfaceBase* shell_surface = GetShellSurfaceBaseForWindow(
        origin_->get()->window()->GetToplevelWindow());
    if (shell_surface)
      shell_surface->set_in_extended_drag(true);
  }

  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(&DragDropOperation::StartDragDropOperation,
                                weak_ptr_factory_.GetWeakPtr()));
}

void DragDropOperation::StartDragDropOperation() {
  uint32_t dnd_operations =
      DndActionsToDragOperations(source_->get()->GetActions());

  base::WeakPtr<DragDropOperation> weak_ptr = weak_ptr_factory_.GetWeakPtr();

  started_ = true;
  gfx::Point drag_start_point = gfx::ToFlooredPoint(drag_start_point_);

  // This triggers a nested run loop that terminates when the drag and drop
  // operation is completed.
  DragOperation op = drag_drop_controller_->StartDragAndDrop(
      std::move(os_exchange_data_), origin_->get()->window()->GetRootWindow(),
      origin_->get()->window(), drag_start_point, dnd_operations,
      event_source_);

  // The instance deleted during StartDragAndDrop's nested RunLoop.
  if (!weak_ptr)
    return;

  // Always reset the in_extended_drag becacuse ExtendedDragSource may be
  // destroyed during nested loop.
  if (origin_->get()) {
    ShellSurfaceBase* shell_surface = GetShellSurfaceBaseForWindow(
        origin_->get()->window()->GetToplevelWindow());
    if (shell_surface)
      shell_surface->set_in_extended_drag(false);
  }

  // In tests, drag_drop_controller_ does not create a nested message loop and
  // so StartDragAndDrop exits before the drag&drop session finishes. In that
  // case the cleanup process shouldn't be made.
  if (drag_drop_controller_->IsDragDropInProgress())
    return;

  if (op != DragOperation::kNone) {
    // Success

    // TODO(crbug.com/994065) This is currently not the actual mime type
    // used by the recipient, just an arbitrary one we pick out of the
    // offered types so we can report back whether or not the drop can
    // succeed. This may need to change in the future.
    source_->get()->Target(mime_type_);

    source_->get()->Action(DragOperationToDndAction(op));
    source_->get()->DndDropPerformed();
    source_->get()->DndFinished();

    // Reset |source_| so it the destructor doesn't try to cancel it.
    source_.reset();
  }

  // On failure the destructor will handle canceling the data source.
  delete this;
}

void DragDropOperation::OnDragStarted() {
  if (!started_) {
    delete this;
  }
}

void DragDropOperation::OnDragActionsChanged(int actions) {
  if (!started_) {
    return;
  }

  DndAction dnd_action = DragOperationsToPreferredDndAction(actions);
  // We send a mime type along with the action to indicate to the application
  // that dropping is/is not currently possible. We do not currently know of
  // any applications that care about the specific mime type until the drop is
  // actually performed.
  if (dnd_action != DndAction::kNone)
    source_->get()->Target(mime_type_);
  else
    source_->get()->Target(std::nullopt);

  source_->get()->Action(dnd_action);
}

void DragDropOperation::OnExtendedDragSourceDestroying(
    ExtendedDragSource* source) {
  ResetExtendedDragSource();
}

void DragDropOperation::ResetExtendedDragSource() {
  DCHECK(extended_drag_source_);
  extended_drag_source_->RemoveObserver(this);
  drag_drop_controller_->set_toplevel_window_drag_delegate(nullptr);
  extended_drag_source_ = nullptr;
}

void DragDropOperation::OnSurfaceDestroying(Surface* surface) {
  DCHECK(surface == origin_->get() || (icon_ && surface == icon_->get()));
  delete this;
}

void DragDropOperation::OnDataSourceDestroying(DataSource* source) {
  DCHECK_EQ(source, source_->get());
  source_.reset();
  LOG(ERROR) << "DataSource was destroyed by client";
  delete this;
}

void DragDropOperation::DragDataReadTimeout() {
  LOG(ERROR) << "DragDataReadTimeout";
}

}  // namespace exo