// 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 "chromeos/ash/services/secure_channel/nearby_connection.h"
#include "base/check.h"
#include "base/containers/flat_map.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/services/secure_channel/file_transfer_update_callback.h"
#include "chromeos/ash/services/secure_channel/public/mojom/nearby_connector.mojom.h"
#include "chromeos/ash/services/secure_channel/public/mojom/secure_channel_types.mojom.h"
#include "chromeos/ash/services/secure_channel/wire_message.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
namespace ash::secure_channel {
namespace {
const char kBluetoothAddressSeparator[] = ":";
} // namespace
// static
NearbyConnection::Factory* NearbyConnection::Factory::factory_instance_ =
nullptr;
// static
std::unique_ptr<Connection> NearbyConnection::Factory::Create(
multidevice::RemoteDeviceRef remote_device,
const std::vector<uint8_t>& eid,
mojom::NearbyConnector* nearby_connector) {
if (factory_instance_)
return factory_instance_->CreateInstance(remote_device, eid,
nearby_connector);
return base::WrapUnique(
new NearbyConnection(remote_device, eid, nearby_connector));
}
// static
void NearbyConnection::Factory::SetFactoryForTesting(Factory* factory) {
factory_instance_ = factory;
}
NearbyConnection::NearbyConnection(multidevice::RemoteDeviceRef remote_device,
const std::vector<uint8_t>& eid,
mojom::NearbyConnector* nearby_connector)
: Connection(remote_device),
nearby_connector_(nearby_connector),
eid_(eid) {
DCHECK(nearby_connector_);
file_payload_listener_receivers_.set_disconnect_handler(base::BindRepeating(
&NearbyConnection::OnFilePayloadListenerRemoteDisconnected,
base::Unretained(this)));
}
NearbyConnection::~NearbyConnection() {
Disconnect();
}
void NearbyConnection::Connect() {
SetStatus(Status::IN_PROGRESS);
nearby_connector_->Connect(
GetRemoteDeviceBluetoothAddressAsVector(), eid_,
message_receiver_.BindNewPipeAndPassRemote(),
nearby_connection_state_listener_.BindNewPipeAndPassRemote(),
base::BindOnce(&NearbyConnection::OnConnectResult,
weak_ptr_factory_.GetWeakPtr()));
}
void NearbyConnection::Disconnect() {
message_sender_.reset();
message_receiver_.reset();
nearby_connection_state_listener_.reset();
file_payload_handler_.reset();
CleanUpPendingFileTransfersOnDisconnect();
SetStatus(Status::DISCONNECTED);
}
std::string NearbyConnection::GetDeviceAddress() {
return remote_device().bluetooth_public_address();
}
void NearbyConnection::SendMessageImpl(std::unique_ptr<WireMessage> message) {
queued_messages_to_send_.emplace(std::move(message));
ProcessQueuedMessagesToSend();
}
void NearbyConnection::RegisterPayloadFileImpl(
int64_t payload_id,
mojom::PayloadFilesPtr payload_files,
FileTransferUpdateCallback file_transfer_update_callback,
base::OnceCallback<void(bool)> registration_result_callback) {
mojo::PendingRemote<mojom::FilePayloadListener>
file_payload_listener_pending_remote;
file_payload_listener_receivers_.Add(
this,
file_payload_listener_pending_remote.InitWithNewPipeAndPassReceiver(),
/*context=*/payload_id);
file_transfer_update_callbacks_.emplace(
payload_id, std::move(file_transfer_update_callback));
file_payload_handler_->RegisterPayloadFile(
payload_id, std::move(payload_files),
std::move(file_payload_listener_pending_remote),
std::move(registration_result_callback));
}
void NearbyConnection::OnMessageReceived(const std::string& message) {
OnBytesReceived(message);
}
void NearbyConnection::OnNearbyConnectionStateChanged(
mojom::NearbyConnectionStep step,
mojom::NearbyConnectionStepResult result) {
SetNearbyConnectionSubStatus(step, result);
}
void NearbyConnection::OnFileTransferUpdate(
mojom::FileTransferUpdatePtr update) {
auto it = file_transfer_update_callbacks_.find(update->payload_id);
if (it == file_transfer_update_callbacks_.end()) {
PA_LOG(WARNING) << "Received transfer update for unregistered file payload "
<< update->payload_id;
Disconnect();
return;
}
bool is_transfer_complete =
update->status != mojom::FileTransferStatus::kInProgress;
it->second.Run(std::move(update));
if (is_transfer_complete) {
file_transfer_update_callbacks_.erase(it);
}
}
void NearbyConnection::CleanUpPendingFileTransfersOnDisconnect() {
// Notify clients of uncompleted file transfers and clean up callbacks and
// corresponding FilePayloadListener receivers when the connection
// drops.
for (auto& id_to_callback : file_transfer_update_callbacks_) {
id_to_callback.second.Run(mojom::FileTransferUpdate::New(
id_to_callback.first, mojom::FileTransferStatus::kCanceled,
/*total_bytes=*/0, /*bytes_transferred=*/0));
}
file_transfer_update_callbacks_.clear();
file_payload_listener_receivers_.Clear();
}
void NearbyConnection::OnFilePayloadListenerRemoteDisconnected() {
int64_t payload_id = file_payload_listener_receivers_.current_context();
auto it = file_transfer_update_callbacks_.find(payload_id);
if (it != file_transfer_update_callbacks_.end()) {
// If the file transfer update callback hasn't been removed by the time the
// corresponding Remote endpoint disconnects, the transfer for this payload
// hasn't completed yet and we need to send a cancelation update.
it->second.Run(mojom::FileTransferUpdate::New(
payload_id, mojom::FileTransferStatus::kCanceled,
/*total_bytes=*/0, /*bytes_transferred=*/0));
file_transfer_update_callbacks_.erase(it);
}
}
std::vector<uint8_t>
NearbyConnection::GetRemoteDeviceBluetoothAddressAsVector() {
std::vector<std::string> hex_bytes_as_strings =
base::SplitString(GetDeviceAddress(), kBluetoothAddressSeparator,
base::WhitespaceHandling::TRIM_WHITESPACE,
base::SplitResult::SPLIT_WANT_ALL);
std::vector<uint8_t> bytes;
for (const std::string& hex_bytes_as_string : hex_bytes_as_strings) {
int byte_value;
base::HexStringToInt(hex_bytes_as_string, &byte_value);
bytes.push_back(static_cast<uint8_t>(byte_value));
}
return bytes;
}
void NearbyConnection::OnConnectResult(
mojo::PendingRemote<mojom::NearbyMessageSender> message_sender,
mojo::PendingRemote<mojom::NearbyFilePayloadHandler> file_payload_handler) {
if (!message_sender) {
PA_LOG(WARNING) << "NearbyConnector returned invalid MessageSender; "
<< "stopping connection attempt.";
Disconnect();
return;
}
if (!file_payload_handler) {
PA_LOG(WARNING) << "NearbyConnector returned invalid FilePayloadHandler; "
<< "stopping connection attempt.";
Disconnect();
return;
}
message_sender_.Bind(std::move(message_sender));
file_payload_handler_.Bind(std::move(file_payload_handler));
message_sender_.set_disconnect_handler(
base::BindOnce(&NearbyConnection::Disconnect, base::Unretained(this)));
file_payload_handler_.set_disconnect_handler(
base::BindOnce(&NearbyConnection::Disconnect, base::Unretained(this)));
message_receiver_.set_disconnect_handler(
base::BindOnce(&NearbyConnection::Disconnect, base::Unretained(this)));
SetStatus(Status::CONNECTED);
}
void NearbyConnection::OnSendMessageResult(bool success) {
OnDidSendMessage(*message_being_sent_, success);
if (success) {
message_being_sent_.reset();
ProcessQueuedMessagesToSend();
return;
}
// Failing to send a message is a fatal error; disconnect.
PA_LOG(WARNING) << "Sending message failed; disconnecting.";
Disconnect();
}
void NearbyConnection::ProcessQueuedMessagesToSend() {
// Message is already being sent.
if (message_being_sent_)
return;
// No pending messages to send.
if (queued_messages_to_send_.empty())
return;
message_being_sent_ = std::move(queued_messages_to_send_.front());
queued_messages_to_send_.pop();
message_sender_->SendMessage(
message_being_sent_->Serialize(),
base::BindOnce(&NearbyConnection::OnSendMessageResult,
base::Unretained(this)));
}
} // namespace ash::secure_channel