// Copyright 2024 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/components/data_migration/testing/fake_nearby_connections.h"
#include <utility>
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/containers/extend.h"
#include "base/files/file_util.h"
#include "base/notimplemented.h"
#include "base/rand_util.h"
#include "chromeos/ash/components/data_migration/constants.h"
#include "chromeos/ash/services/nearby/public/mojom/nearby_connections_types.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace data_migration {
namespace {
constexpr std::string_view kTestAuthToken = "test-auth-token";
} // namespace
FakeNearbyConnections::RegisteredFilePayload::RegisteredFilePayload() = default;
FakeNearbyConnections::RegisteredFilePayload::RegisteredFilePayload(
base::File input_file_in,
base::File output_file_in)
: input_file(std::move(input_file_in)),
output_file(std::move(output_file_in)) {}
FakeNearbyConnections::RegisteredFilePayload::RegisteredFilePayload(
RegisteredFilePayload&&) = default;
FakeNearbyConnections::RegisteredFilePayload&
FakeNearbyConnections::RegisteredFilePayload::operator=(
RegisteredFilePayload&&) = default;
FakeNearbyConnections::RegisteredFilePayload::~RegisteredFilePayload() =
default;
FakeNearbyConnections::FakeNearbyConnections(
std::string_view remote_endpoint_id)
: remote_endpoint_id_(remote_endpoint_id) {
CHECK(!remote_endpoint_id_.empty());
}
FakeNearbyConnections::~FakeNearbyConnections() = default;
bool FakeNearbyConnections::SendFile(int64_t payload_id,
std::vector<uint8_t>* transferred_bytes) {
if (transferred_bytes) {
transferred_bytes->clear();
}
if (!remote_to_local_payload_listener_.is_bound()) {
LOG(ERROR) << "Payload listener not bound. Cannot send file yet.";
return false;
}
auto registered_files_iter = registered_files_.find(payload_id);
if (registered_files_iter == registered_files_.end()) {
LOG(ERROR) << "Payload id " << payload_id
<< " and its corresponding path not registered yet.";
return false;
}
RegisteredFilePayload file_handles = std::move(registered_files_iter->second);
registered_files_.erase(registered_files_iter);
remote_to_local_payload_listener_->OnPayloadReceived(
remote_endpoint_id_,
::nearby::connections::mojom::Payload::New(
payload_id, ::nearby::connections::mojom::PayloadContent::NewFile(
::nearby::connections::mojom::FilePayload::New(
std::move(file_handles.input_file)))));
// For a successful case, break the file into 4 equal size chunks. For any
// failure case, transfer one chunk of the file and then fail.
PayloadStatus final_status = final_file_payload_status_.contains(payload_id)
? final_file_payload_status_.at(payload_id)
: PayloadStatus::kSuccess;
size_t num_chunks_to_transfer =
final_status == PayloadStatus::kSuccess ? test_file_num_chunks_ : 1;
size_t total_bytes_transferred = 0;
for (size_t chunk_idx = 0; chunk_idx < num_chunks_to_transfer; ++chunk_idx) {
const std::vector<uint8_t> new_chunk = base::RandBytesAsVector(
test_file_size_in_bytes_ / test_file_num_chunks_);
CHECK(!new_chunk.empty());
base::File& output_file = file_handles.output_file;
if (!output_file.WriteAtCurrentPosAndCheck(new_chunk) ||
!output_file.Flush()) {
LOG(ERROR) << "Failed to write file chunk for payload " << payload_id;
return false;
}
if (transferred_bytes) {
base::Extend(*transferred_bytes, new_chunk);
}
total_bytes_transferred += new_chunk.size();
remote_to_local_payload_listener_->OnPayloadTransferUpdate(
remote_endpoint_id_,
::nearby::connections::mojom::PayloadTransferUpdate::New(
payload_id, PayloadStatus::kInProgress, test_file_size_in_bytes_,
total_bytes_transferred));
}
remote_to_local_payload_listener_->OnPayloadTransferUpdate(
remote_endpoint_id_,
::nearby::connections::mojom::PayloadTransferUpdate::New(
payload_id, final_status, test_file_size_in_bytes_,
total_bytes_transferred));
return true;
}
void FakeNearbyConnections::SetFinalFilePayloadStatus(PayloadStatus status,
int64_t payload_id) {
final_file_payload_status_[payload_id] = status;
}
bool FakeNearbyConnections::SendBytesPayload(int64_t payload_id,
const std::string& bytes) {
if (!remote_to_local_payload_listener_.is_bound()) {
LOG(ERROR) << "Payload listener not bound. Cannot send bytes yet.";
return false;
}
remote_to_local_payload_listener_->OnPayloadReceived(
remote_endpoint_id_,
::nearby::connections::mojom::Payload::New(
payload_id,
::nearby::connections::mojom::PayloadContent::NewBytes(
::nearby::connections::mojom::BytesPayload::New(
std::vector<uint8_t>(bytes.begin(), bytes.end())))));
remote_to_local_payload_listener_->OnPayloadTransferUpdate(
remote_endpoint_id_,
::nearby::connections::mojom::PayloadTransferUpdate::New(
payload_id, PayloadStatus::kSuccess, bytes.size(), bytes.size()));
return true;
}
bool FakeNearbyConnections::SimulateRemoteDisconnect() {
if (!connection_listener_.is_bound()) {
return false;
}
auto connection_listener = std::move(connection_listener_);
DisconnectFromEndpoint(kServiceId, remote_endpoint_id_, base::DoNothing());
connection_listener->OnDisconnected(remote_endpoint_id_);
return true;
}
void FakeNearbyConnections::StartAdvertising(
const std::string& service_id,
const std::vector<uint8_t>& endpoint_info,
::nearby::connections::mojom::AdvertisingOptionsPtr options,
mojo::PendingRemote<
::nearby::connections::mojom::ConnectionLifecycleListener> listener,
StartAdvertisingCallback callback) {
if (service_id != kServiceId) {
GTEST_FAIL() << "StartAdvertising() call invalid. service_id="
<< service_id;
}
if (is_advertising_) {
std::move(callback).Run(Status::kAlreadyAdvertising);
return;
}
is_advertising_ = true;
connection_listener_.reset();
connection_listener_.Bind(std::move(listener));
// 1) Advertising starts successfully.
std::move(callback).Run(Status::kSuccess);
// 2) Immediately notify the ChromeOS target device of a connection
// initiation. This simulates immediate discovery in the real world.
//
// These are essential options for data_migration to work. If they're not
// set properly, the `listener` will not receive any incoming connections,
// which reflects reality.
if (options->strategy ==
::nearby::connections::mojom::Strategy::kP2pPointToPoint &&
options->allowed_mediums->bluetooth) {
connection_listener_->OnConnectionInitiated(
remote_endpoint_id_,
::nearby::connections::mojom::ConnectionInfo::New(
kTestAuthToken.data(),
/*raw_authentication_token=*/base::RandBytesAsVector(64),
/*endpoint_info=*/std::vector<uint8_t>(64, 0),
/*is_incoming_connection=*/true));
} else {
GTEST_FAIL() << "Invalid advertising options. strategy="
<< options->strategy
<< " bluetooth=" << options->allowed_mediums->bluetooth;
}
}
void FakeNearbyConnections::StopAdvertising(const std::string& service_id,
StopAdvertisingCallback callback) {
if (service_id != kServiceId || !is_advertising_) {
GTEST_FAIL() << "StopAdvertising() call invalid. service_id=" << service_id
<< " is_advertising_=" << is_advertising_;
}
is_advertising_ = false;
std::move(callback).Run(Status::kSuccess);
}
void FakeNearbyConnections::StartDiscovery(
const std::string& service_id,
::nearby::connections::mojom::DiscoveryOptionsPtr options,
mojo::PendingRemote<::nearby::connections::mojom::EndpointDiscoveryListener>
listener,
StartDiscoveryCallback callback) {
NOTIMPLEMENTED();
}
void FakeNearbyConnections::StopDiscovery(const std::string& service_id,
StopDiscoveryCallback callback) {
NOTIMPLEMENTED();
}
void FakeNearbyConnections::InjectBluetoothEndpoint(
const std::string& service_id,
const std::string& endpoint_id,
const std::vector<uint8_t>& endpoint_info,
const std::vector<uint8_t>& remote_bluetooth_mac_address,
InjectBluetoothEndpointCallback callback) {
NOTIMPLEMENTED();
}
void FakeNearbyConnections::RequestConnection(
const std::string& service_id,
const std::vector<uint8_t>& endpoint_info,
const std::string& endpoint_id,
::nearby::connections::mojom::ConnectionOptionsPtr options,
mojo::PendingRemote<
::nearby::connections::mojom::ConnectionLifecycleListener> listener,
RequestConnectionCallback callback) {
NOTIMPLEMENTED();
}
void FakeNearbyConnections::DisconnectFromEndpoint(
const std::string& service_id,
const std::string& endpoint_id,
DisconnectFromEndpointCallback callback) {
if (service_id == kServiceId && endpoint_id == remote_endpoint_id_) {
connection_listener_.reset();
remote_to_local_payload_listener_.reset();
registered_files_.clear();
} else {
GTEST_FAIL() << "DisconnectFromEndpoint() call invalid. service_id="
<< service_id << " endpoint_id=" << endpoint_id;
}
std::move(callback).Run(Status::kSuccess);
}
void FakeNearbyConnections::AcceptConnection(
const std::string& service_id,
const std::string& endpoint_id,
mojo::PendingRemote<::nearby::connections::mojom::PayloadListener> listener,
AcceptConnectionCallback callback) {
// `service_id != kServiceId` - This class never initiates a connection for
// a service other than data migration, so accepting a connection before an
// initiation is out of order.
//
// `!connection_listener_.is_bound()` - The ChromeOS target device tried to
// accept a connection before it was discovered. Also out of order.
if (service_id != kServiceId || !connection_listener_.is_bound()) {
GTEST_FAIL() << "AcceptConnection() call invalid. service_id=" << service_id
<< " connection_listener_=" << connection_listener_.is_bound();
}
if (remote_to_local_payload_listener_.is_bound()) {
std::move(callback).Run(Status::kAlreadyConnectedToEndpoint);
return;
}
remote_to_local_payload_listener_.Bind(std::move(listener));
std::move(callback).Run(Status::kSuccess);
// In reality, the user would be prompted with a visual pin at this point and
// need to confirm the transfer on the remote device before moving on. For
// tests, assume this passes and establish the connection immediately
// (ChromeOS just sent the remote device an "accept connection", and now
// the remote device sends an "accept connection" back).
connection_listener_->OnConnectionAccepted(remote_endpoint_id_);
if (connection_established_listener_) {
connection_established_listener_.Run();
}
}
void FakeNearbyConnections::RejectConnection(
const std::string& service_id,
const std::string& endpoint_id,
RejectConnectionCallback callback) {
NOTIMPLEMENTED();
}
void FakeNearbyConnections::SendPayload(
const std::string& service_id,
const std::vector<std::string>& endpoint_ids,
::nearby::connections::mojom::PayloadPtr payload,
SendPayloadCallback callback) {
if (service_id != kServiceId) {
GTEST_FAIL() << "Sending payload to unexpected service_id " << service_id;
}
if (!base::Contains(endpoint_ids, remote_endpoint_id_)) {
std::move(callback).Run(Status::kEndpointUnknown);
return;
}
// `remote_to_local_payload_listener_` is only bound after both sides of the
// connection have been accepted. Although `remote_to_local_payload_listener_`
// is not used in this method, it reflects reality. The local device cannot
// send a payload until the connection is formed.
if (!remote_to_local_payload_listener_.is_bound()) {
std::move(callback).Run(Status::kOutOfOrderApiCall);
return;
}
if (local_to_remote_payload_listener_) {
local_to_remote_payload_listener_.Run(std::move(payload));
}
std::move(callback).Run(Status::kSuccess);
}
void FakeNearbyConnections::CancelPayload(const std::string& service_id,
int64_t payload_id,
CancelPayloadCallback callback) {
registered_files_.erase(payload_id);
std::move(callback).Run(Status::kSuccess);
}
void FakeNearbyConnections::StopAllEndpoints(
const std::string& service_id,
StopAllEndpointsCallback callback) {
DisconnectFromEndpoint(service_id, remote_endpoint_id_, std::move(callback));
}
void FakeNearbyConnections::InitiateBandwidthUpgrade(
const std::string& service_id,
const std::string& endpoint_id,
InitiateBandwidthUpgradeCallback callback) {
NOTIMPLEMENTED();
}
// This should happen before `FakeNearbyConnections::SendFile()`. This reflects
// the order of operations in reality.
void FakeNearbyConnections::RegisterPayloadFile(
const std::string& service_id,
int64_t payload_id,
base::File input_file,
base::File output_file,
RegisterPayloadFileCallback callback) {
if (service_id != kServiceId) {
GTEST_FAIL() << "RegisterPayloadFile() call invalid. service_id="
<< service_id;
}
Status result = Status::kSuccess;
if (register_payload_file_result_generator_) {
result = register_payload_file_result_generator_.Run();
}
if (result == Status::kSuccess) {
registered_files_[payload_id] =
RegisteredFilePayload(std::move(input_file), std::move(output_file));
}
std::move(callback).Run(result);
}
void FakeNearbyConnections::RequestConnectionV3(
const std::string& service_id,
ash::nearby::presence::mojom::PresenceDevicePtr remote_device,
::nearby::connections::mojom::ConnectionOptionsPtr connection_options,
mojo::PendingRemote<::nearby::connections::mojom::ConnectionListenerV3>
listener,
RequestConnectionV3Callback callback) {
NOTIMPLEMENTED();
}
void FakeNearbyConnections::AcceptConnectionV3(
const std::string& service_id,
ash::nearby::presence::mojom::PresenceDevicePtr remote_device,
mojo::PendingRemote<::nearby::connections::mojom::PayloadListenerV3>
listener,
AcceptConnectionV3Callback callback) {
NOTIMPLEMENTED();
}
void FakeNearbyConnections::RejectConnectionV3(
const std::string& service_id,
ash::nearby::presence::mojom::PresenceDevicePtr remote_device,
RejectConnectionV3Callback callback) {
NOTIMPLEMENTED();
}
void FakeNearbyConnections::DisconnectFromDeviceV3(
const std::string& service_id,
ash::nearby::presence::mojom::PresenceDevicePtr remote_device,
DisconnectFromDeviceV3Callback callback) {
NOTIMPLEMENTED();
}
void FakeNearbyConnections::RegisterServiceWithPresenceDeviceProvider(
const std::string& service_id) {
NOTIMPLEMENTED();
}
} // namespace data_migration