chromium/chrome/browser/nearby_sharing/nearby_per_session_discovery_manager.cc

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

#include "chrome/browser/nearby_sharing/nearby_per_session_discovery_manager.h"

#include <string>

#include "base/containers/contains.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/nearby_sharing/attachment.h"
#include "chrome/browser/nearby_sharing/nearby_confirmation_manager.h"
#include "chrome/browser/ui/webui/nearby_share/nearby_share.mojom-forward.h"
#include "components/cross_device/logging/logging.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"

namespace {
std::optional<nearby_share::mojom::TransferStatus> GetTransferStatus(
    const TransferMetadata& transfer_metadata) {
  switch (transfer_metadata.status()) {
    case TransferMetadata::Status::kAwaitingLocalConfirmation:
      return nearby_share::mojom::TransferStatus::kAwaitingLocalConfirmation;
    case TransferMetadata::Status::kAwaitingRemoteAcceptance:
      return nearby_share::mojom::TransferStatus::kAwaitingRemoteAcceptance;
    case TransferMetadata::Status::kComplete:
      return nearby_share::mojom::TransferStatus::kComplete;
    case TransferMetadata::Status::kInProgress:
      return nearby_share::mojom::TransferStatus::kInProgress;
    case TransferMetadata::Status::kRejected:
      return nearby_share::mojom::TransferStatus::kRejected;
    case TransferMetadata::Status::kTimedOut:
      return nearby_share::mojom::TransferStatus::kTimedOut;
    case TransferMetadata::Status::kUnsupportedAttachmentType:
      return nearby_share::mojom::TransferStatus::kUnsupportedAttachmentType;
    case TransferMetadata::Status::kMediaUnavailable:
      return nearby_share::mojom::TransferStatus::kMediaUnavailable;
    case TransferMetadata::Status::kNotEnoughSpace:
      return nearby_share::mojom::TransferStatus::kNotEnoughSpace;
    case TransferMetadata::Status::kFailed:
      return nearby_share::mojom::TransferStatus::kFailed;
    case TransferMetadata::Status::kAwaitingRemoteAcceptanceFailed:
      return nearby_share::mojom::TransferStatus::
          kAwaitingRemoteAcceptanceFailed;
    case TransferMetadata::Status::kUnknown:
      return nearby_share::mojom::TransferStatus::kUnknown;
    case TransferMetadata::Status::kConnecting:
      return nearby_share::mojom::TransferStatus::kConnecting;
    case TransferMetadata::Status::kCancelled:
      return nearby_share::mojom::TransferStatus::kCancelled;
    case TransferMetadata::Status::kDecodeAdvertisementFailed:
      return nearby_share::mojom::TransferStatus::kDecodeAdvertisementFailed;
    case TransferMetadata::Status::kMissingTransferUpdateCallback:
      return nearby_share::mojom::TransferStatus::
          kMissingTransferUpdateCallback;
    case TransferMetadata::Status::kMissingShareTarget:
      return nearby_share::mojom::TransferStatus::kMissingShareTarget;
    case TransferMetadata::Status::kMissingEndpointId:
      return nearby_share::mojom::TransferStatus::kMissingEndpointId;
    case TransferMetadata::Status::kMissingPayloads:
      return nearby_share::mojom::TransferStatus::kMissingPayloads;
    case TransferMetadata::Status::kPairedKeyVerificationFailed:
      return nearby_share::mojom::TransferStatus::kPairedKeyVerificationFailed;
    case TransferMetadata::Status::kInvalidIntroductionFrame:
      return nearby_share::mojom::TransferStatus::kInvalidIntroductionFrame;
    case TransferMetadata::Status::kIncompletePayloads:
      return nearby_share::mojom::TransferStatus::kIncompletePayloads;
    case TransferMetadata::Status::kFailedToCreateShareTarget:
      return nearby_share::mojom::TransferStatus::kFailedToCreateShareTarget;
    case TransferMetadata::Status::kFailedToInitiateOutgoingConnection:
      return nearby_share::mojom::TransferStatus::
          kFailedToInitiateOutgoingConnection;
    case TransferMetadata::Status::kFailedToReadOutgoingConnectionResponse:
      return nearby_share::mojom::TransferStatus::
          kFailedToReadOutgoingConnectionResponse;
    case TransferMetadata::Status::kUnexpectedDisconnection:
      return nearby_share::mojom::TransferStatus::kUnexpectedDisconnection;
    case TransferMetadata::Status::kMediaDownloading:
    case TransferMetadata::Status::kExternalProviderLaunched:
      // Ignore all other transfer status updates.
      return std::nullopt;
  }
}

std::string GetDeviceIdForLogs(const ShareTarget& share_target) {
  return share_target.device_id
             ? base::HexEncode(share_target.device_id.value())
             : "[null]";
}

}  // namespace

NearbyPerSessionDiscoveryManager::NearbyPerSessionDiscoveryManager(
    NearbySharingService* nearby_sharing_service,
    std::vector<std::unique_ptr<Attachment>> attachments)
    : nearby_sharing_service_(nearby_sharing_service),
      attachments_(std::move(attachments)) {
  nearby_sharing_service_->AddObserver(this);
}

NearbyPerSessionDiscoveryManager::~NearbyPerSessionDiscoveryManager() {
  StopDiscovery(base::DoNothing());
  observers_set_.Clear();
  nearby_sharing_service_->RemoveObserver(this);
  base::UmaHistogramEnumeration(
      "Nearby.Share.Discovery.FurthestDiscoveryProgress", furthest_progress_);
  base::UmaHistogramCounts100(
      "Nearby.Share.Discovery.NumShareTargets.Discovered", num_discovered_);
  base::UmaHistogramCounts100("Nearby.Share.Discovery.NumShareTargets.Lost",
                              num_lost_);
}

void NearbyPerSessionDiscoveryManager::OnTransferUpdate(
    const ShareTarget& share_target,
    const TransferMetadata& transfer_metadata) {
  if (!transfer_update_listener_.is_bound()) {
    // This can happen when registering the send surface and an existing
    // transfer is happening or recently happened.
    CD_LOG(VERBOSE, Feature::NS)
        << __func__
        << ": transfer_update_listener_ is not is_bound(), cannot "
           "forward transfer updates";
    return;
  }

  CD_LOG(VERBOSE, Feature::NS)
      << __func__ << ": Nearby per-session discovery manager: "
      << "Transfer update for share target with ID " << share_target.id << ": "
      << TransferMetadata::StatusToString(transfer_metadata.status());

  std::optional<nearby_share::mojom::TransferStatus> status =
      GetTransferStatus(transfer_metadata);

  if (!status) {
    CD_LOG(VERBOSE, Feature::NS)
        << __func__ << ": Nearby per-session discovery manager: "
        << " skipping status update, no mojo mapping defined yet.";
    return;
  }

  transfer_update_listener_->OnTransferUpdate(*status,
                                              transfer_metadata.token());
}

void NearbyPerSessionDiscoveryManager::OnShareTargetDiscovered(
    ShareTarget share_target) {
  CD_LOG(VERBOSE, Feature::NS)
      << "NearbyPerSessionDiscoveryManager::" << __func__
      << ": id=" << share_target.id
      << ", device_id=" << GetDeviceIdForLogs(share_target);
  // Update metrics.
  UpdateFurthestDiscoveryProgressIfNecessary(
      DiscoveryProgress::kDiscoveredShareTargetNothingSent);
  if (!base::Contains(discovered_share_targets_, share_target.id)) {
    ++num_discovered_;
    if (num_discovered_ == 1) {
      base::UmaHistogramMediumTimes(
          "Nearby.Share.Discovery.Delay.FromStartDiscoveryToFirstDiscovery",
          base::TimeTicks::Now() - *discovery_start_time_);
    }
    base::UmaHistogramMediumTimes(
        "Nearby.Share.Discovery.Delay.FromStartDiscoveryToAnyDiscovery",
        base::TimeTicks::Now() - *discovery_start_time_);
  }

  // Dedup by the more stable device ID if possible.
  if (share_target.device_id) {
    auto it =
        base::ranges::find(discovered_share_targets_, share_target.device_id,
                           [](const auto& id_share_target_pair) {
                             return id_share_target_pair.second.device_id;
                           });

    if (it != discovered_share_targets_.end()) {
      CD_LOG(VERBOSE, Feature::NS)
          << "NearbyPerSessionDiscoveryManager::" << __func__
          << ": Removing previously discovered share target with "
          << "identical device_id=" << GetDeviceIdForLogs(share_target);
      OnShareTargetLost(it->second);
    }
  }

  discovered_share_targets_.insert_or_assign(share_target.id, share_target);
  share_target_listener_->OnShareTargetDiscovered(share_target);
}

void NearbyPerSessionDiscoveryManager::AddDiscoveryObserver(
    ::mojo::PendingRemote<nearby_share::mojom::DiscoveryObserver> observer) {
  observers_set_.Add(std::move(observer));
}

void NearbyPerSessionDiscoveryManager::OnShareTargetLost(
    ShareTarget share_target) {
  CD_LOG(VERBOSE, Feature::NS)
      << "NearbyPerSessionDiscoveryManager::" << __func__
      << ": id=" << share_target.id
      << ", device_id=" << GetDeviceIdForLogs(share_target);

  // It is possible that we already removed a ShareTarget from the map when
  // deduping by ShareTarget device_id.
  if (!base::Contains(discovered_share_targets_, share_target.id)) {
    CD_LOG(VERBOSE, Feature::NS)
        << "NearbyPerSessionDiscoveryManager::" << __func__
        << ": Share target id=" << share_target.id
        << " already removed. Taking no action.";
    return;
  }

  ++num_lost_;
  discovered_share_targets_.erase(share_target.id);
  share_target_listener_->OnShareTargetLost(share_target);
}

void NearbyPerSessionDiscoveryManager::StartDiscovery(
    mojo::PendingRemote<nearby_share::mojom::ShareTargetListener> listener,
    StartDiscoveryCallback callback) {
  if (nearby_sharing_service_->IsTransferring() ||
      nearby_sharing_service_->IsScanning() ||
      nearby_sharing_service_->IsConnecting()) {
    // Is there is currently a file transfer ongoing, return early with the
    // corresponding error code.
    std::move(callback).Run(nearby_share::mojom::StartDiscoveryResult::
                                kErrorInProgressTransferring);
    return;
  }

  discovery_start_time_ = base::TimeTicks::Now();

  // Starting discovery again closes any previous discovery session.
  share_target_listener_.reset();
  share_target_listener_.Bind(std::move(listener));
  // NOTE: Previously we set a disconnect handler here that called
  // UnregisterSendSurface, but this causes transfer updates to stop flowing to
  // to the UI. Instead, we rely on the destructor's call to
  // UnregisterSendSurface which will trigger when the share sheet goes away.

  NearbySharingService::StatusCodes status =
      nearby_sharing_service_->RegisterSendSurface(
          this, this, NearbySharingService::SendSurfaceState::kForeground);
  base::UmaHistogramEnumeration("Nearby.Share.Discovery.StartDiscovery",
                                status);
  if (status != NearbySharingService::StatusCodes::kOk) {
    CD_LOG(WARNING, Feature::NS)
        << __func__ << ": Failed to register send surface";
    UpdateFurthestDiscoveryProgressIfNecessary(
        DiscoveryProgress::kFailedToStartDiscovery);
    share_target_listener_.reset();

    nearby_share::mojom::StartDiscoveryResult errorStatus;
    switch (status) {
      case NearbySharingService::StatusCodes::kNoAvailableConnectionMedium:
        errorStatus =
            nearby_share::mojom::StartDiscoveryResult::kNoConnectionMedium;
        break;
      default:
        errorStatus = nearby_share::mojom::StartDiscoveryResult::kErrorGeneric;
    }
    std::move(callback).Run(errorStatus);

    return;
  }

  UpdateFurthestDiscoveryProgressIfNecessary(
      DiscoveryProgress::kStartedDiscoveryNothingFound);

  // Once this object is registered as send surface, we stay registered until
  // UnregisterSendSurface is called so that the transfer update listeners can
  // get updates even if Discovery is stopped.
  registered_as_send_surface_ = true;
  std::move(callback).Run(nearby_share::mojom::StartDiscoveryResult::kSuccess);
}

void NearbyPerSessionDiscoveryManager::StopDiscovery(
    base::OnceClosure callback) {
  if (registered_as_send_surface_) {
    NearbySharingService::StatusCodes status =
        nearby_sharing_service_->UnregisterSendSurface(this, this);
    base::UmaHistogramEnumeration(
        "Nearby.Share.Discovery.UnregisterSendSurface", status);
    if (status != NearbySharingService::StatusCodes::kOk) {
      CD_LOG(WARNING, Feature::NS)
          << __func__ << ": Failed to unregister send surface";
    }
    registered_as_send_surface_ = false;
  }

  share_target_listener_.reset();
  std::move(callback).Run();
}

void NearbyPerSessionDiscoveryManager::SelectShareTarget(
    const base::UnguessableToken& share_target_id,
    SelectShareTargetCallback callback) {
  DCHECK(share_target_listener_.is_bound());
  DCHECK(!transfer_update_listener_.is_bound());

  auto iter = discovered_share_targets_.find(share_target_id);
  bool look_up_share_target_success = iter != discovered_share_targets_.end();
  base::UmaHistogramBoolean("Nearby.Share.Discovery.LookUpSelectedShareTarget",
                            look_up_share_target_success);
  if (!look_up_share_target_success) {
    CD_LOG(VERBOSE, Feature::NS)
        << __func__
        << ": Unknown share target selected: id=" << share_target_id;
    UpdateFurthestDiscoveryProgressIfNecessary(
        DiscoveryProgress::kFailedToLookUpSelectedShareTarget);
    std::move(callback).Run(
        nearby_share::mojom::SelectShareTargetResult::kInvalidShareTarget,
        mojo::NullReceiver(), mojo::NullRemote());
    return;
  }

  // Bind update listener before calling the sharing service to get all updates.
  mojo::PendingReceiver<nearby_share::mojom::TransferUpdateListener> receiver =
      transfer_update_listener_.BindNewPipeAndPassReceiver();
  transfer_update_listener_.reset_on_disconnect();

  base::UmaHistogramCounts100(
      "Nearby.Share.Discovery.NumShareTargets.PresentWhenSendStarts",
      discovered_share_targets_.size());
  base::UmaHistogramMediumTimes(
      "Nearby.Share.Discovery.Delay.FromStartDiscoveryToStartSend",
      base::TimeTicks::Now() - *discovery_start_time_);

  NearbySharingService::StatusCodes status =
      nearby_sharing_service_->SendAttachments(iter->second,
                                               std::move(attachments_));
  base::UmaHistogramEnumeration("Nearby.Share.Discovery.StartSend", status);

  // If the send call succeeded, we expect OnTransferUpdate() to be called next.
  if (status == NearbySharingService::StatusCodes::kOk) {
    UpdateFurthestDiscoveryProgressIfNecessary(DiscoveryProgress::kStartedSend);
    mojo::PendingRemote<nearby_share::mojom::ConfirmationManager> remote;
    mojo::MakeSelfOwnedReceiver(std::make_unique<NearbyConfirmationManager>(
                                    nearby_sharing_service_, iter->second),
                                remote.InitWithNewPipeAndPassReceiver());
    std::move(callback).Run(nearby_share::mojom::SelectShareTargetResult::kOk,
                            std::move(receiver), std::move(remote));
    return;
  }

  CD_LOG(VERBOSE, Feature::NS)
      << __func__ << ": Failed to start send to share target";
  UpdateFurthestDiscoveryProgressIfNecessary(
      DiscoveryProgress::kFailedToStartSend);
  transfer_update_listener_.reset();
  std::move(callback).Run(nearby_share::mojom::SelectShareTargetResult::kError,
                          mojo::NullReceiver(), mojo::NullRemote());
}

void NearbyPerSessionDiscoveryManager::GetPayloadPreview(
    GetPayloadPreviewCallback callback) {
  // TODO(crbug.com/1158627): Extract this which is very similar to logic in
  // nearby share mojo traits.
  nearby_share::mojom::PayloadPreviewPtr payload_preview =
      nearby_share::mojom::PayloadPreview::New();
  payload_preview->file_count = 0;
  payload_preview->share_type = nearby_share::mojom::ShareType::kText;
  if (attachments_.empty()) {
    // Return with an empty text attachment.
    std::move(callback).Run(std::move(payload_preview));
    return;
  }

  // We have at least 1 attachment, use that one for the default description.
  auto& attachment = attachments_[0];
  payload_preview->description = attachment->GetDescription();

  if (attachment->family() == Attachment::Family::kFile)
    payload_preview->file_count = attachments_.size();

  if (payload_preview->file_count > 1) {
    payload_preview->share_type =
        nearby_share::mojom::ShareType::kMultipleFiles;
  } else {
    payload_preview->share_type = attachment->GetShareType();
  }

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

void NearbyPerSessionDiscoveryManager::OnNearbyProcessStopped() {
  for (auto& remote : observers_set_) {
    remote->OnNearbyProcessStopped();
  }
}

void NearbyPerSessionDiscoveryManager::OnStartDiscoveryResult(bool success) {
  for (auto& remote : observers_set_) {
    remote->OnStartDiscoveryResult(success);
  }
}

void NearbyPerSessionDiscoveryManager::
    UpdateFurthestDiscoveryProgressIfNecessary(DiscoveryProgress progress) {
  if (static_cast<int>(progress) > static_cast<int>(furthest_progress_))
    furthest_progress_ = progress;
}