// 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/data_migration.h"
#include <cstdint>
#include <optional>
#include <vector>
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/time/time.h"
#include "third_party/nearby/src/internal/platform/byte_array.h"
#include "third_party/nearby/src/internal/platform/byte_utils.h"
namespace data_migration {
namespace {
std::vector<uint8_t> BuildEndpointInfo() {
// Must be < 131:
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/nearby/src/connections/implementation/bluetooth_device_name.h;l=70;drc=084f7ebd8847cfb68191f787dda192644377e6ad
static constexpr size_t kEndpointInfoLength = 64;
// TODO(esum): Fill with real content and comment. Currently, endpoint info
// is still not used/required.
return std::vector<uint8_t>(kEndpointInfoLength, 0);
}
} // namespace
DataMigration::DataMigration(
std::unique_ptr<NearbyConnectionsManager> nearby_connections_manager)
: nearby_connections_manager_(std::move(nearby_connections_manager)) {
CHECK(nearby_connections_manager_);
}
// Do not use `nearby_connections_manager_` or anything that depends on it
// here. Any shutdown logic should go in `DataMigration::Shutdown()`.
DataMigration::~DataMigration() = default;
// Part of the KeyedService shutdown design. Must ensure all nearby connection
// activity is stopped here. Note `DataMigration` is expected to be destroyed
// shortly after this.
void DataMigration::Shutdown() {
VLOG(4) << "__func__";
// Ensure any pending callbacks do not run while tearing everything down.
weak_factory_.InvalidateWeakPtrs();
connected_device_.reset();
nearby_connections_manager_->Shutdown();
}
void DataMigration::StartAdvertising() {
VLOG(1) << "DataMigration advertising starting";
CHECK(!connected_device_) << "Nearby connection already established";
// `NearbyConnectionsManagerImpl` internally uses the arguments below to build
// the following `AdvertisingOptions`:
// * strategy - kP2pPointToPoint
// * mediums
// * bluetooth - true
// * every other medium - false
// * auto_upgrade_bandwidth - true
// * enforce_topology_constraints - true
// * enable_bluetooth_listening - false
// * From docs: "this allows listening on incoming Bluetooth Classic
// connections while BLE advertising". Should not be an issue because
// BLE is a disabled advertising medium to begin with.
// * enable_webrtc_listening - false
// * fast_advertisement_service_uuid - Some internal immutable value.
nearby_connections_manager_->StartAdvertising(
// `PowerLevel::kHighPower` matches what cros quick start uses and is
// required by the `NearbyConnectionsManagerImpl` to use the bluetooth
// classic medium.
BuildEndpointInfo(), this,
NearbyConnectionsManager::PowerLevel::kHighPower,
// This causes `NearbyConnectionsManagerImpl` to disable all wifi-related
// advertisement mechanisms (leaving only bluetooth classic). Note this
// does not affect the medium for the main connection over which
// payloads are transferred.
nearby_share::mojom::DataUsage::kOffline,
base::BindOnce(&DataMigration::OnStartAdvertising,
weak_factory_.GetWeakPtr()));
}
void DataMigration::OnStartAdvertising(
NearbyConnectionsManager::ConnectionsStatus status) {
if (status != NearbyConnectionsManager::ConnectionsStatus::kSuccess) {
// See the `NearbyConnections::StartAdvertising()` API. None of the error
// codes should apply here, but it's not worth crashing the browser process
// in prod if this happens.
// TODO(esum): Add metrics how often this is hit.
LOG(DFATAL) << "DataMigration failed to start advertising with status="
<< status;
}
}
void DataMigration::OnStopAdvertising(
NearbyConnectionsManager::ConnectionsStatus status) {
// Mojo docs claim this can never fail, but this is not worth crashing the
// browser process even so. If advertising keeps running, it shouldn't cause
// this class to fail; it's just less optimal.
if (status != NearbyConnectionsManager::ConnectionsStatus::kSuccess) {
LOG(DFATAL) << "DataMigration failed to stop advertising with status="
<< status;
}
}
void DataMigration::OnIncomingConnectionInitiated(
const std::string& endpoint_id,
const std::vector<uint8_t>& endpoint_info) {
// Note `NearbyConnectionsManagerImpl` automatically accepts this incoming
// connection internally. This leaves the outcome in the hands of the remote
// device, who must accept the connection as well before the 2 sides can start
// exchanging payloads.
//
// This matches the DataMigration design because the user is expected to
// verify that the 4 digit pin matches on both devices, and hit "accept" on
// the remote device (the source of the data) for the connection to be
// established.
std::optional<std::vector<uint8_t>> auth_token =
nearby_connections_manager_->GetRawAuthenticationToken(endpoint_id);
CHECK(auth_token) << "Auth token missing. Should always be available because "
"connection was just initiated.";
::nearby::ByteArray auth_token_as_byte_array =
::nearby::ByteArray(std::string(auth_token->begin(), auth_token->end()));
// TODO(esum):
// * Check if `::nearby::ByteUtils::ToFourDigitString()` needs to run in a
// sandboxed process.
// * Display the pin in an actual UI. Logs are used temporarily here for
// developers.
// * Account for multiple incoming connection requests when the UI is built.
VLOG(1) << "DataMigration connection requested with pin="
<< ::nearby::ByteUtils::ToFourDigitString(auth_token_as_byte_array);
}
void DataMigration::OnIncomingConnectionAccepted(
const std::string& endpoint_id,
const std::vector<uint8_t>& endpoint_info,
NearbyConnection* connection) {
if (connected_device_) {
// Corner case should rarely happen, but only one data migration can be
// active at a time.
LOG(WARNING) << "DataMigration already active with another device. "
"Disconnecting from incoming endpoint.";
connection->Close();
return;
}
VLOG(1) << "DataMigration connection accepted";
// Multiple parallel transfers is not supported, so there's no reason to
// continue advertising at this point.
nearby_connections_manager_->StopAdvertising(base::BindOnce(
&DataMigration::OnStopAdvertising, weak_factory_.GetWeakPtr()));
connected_device_.emplace(connection, nearby_connections_manager_.get());
connection->SetDisconnectionListener(base::BindOnce(
&DataMigration::OnDeviceDisconnected, weak_factory_.GetWeakPtr()));
}
void DataMigration::OnDeviceDisconnected() {
CHECK(connected_device_);
// Note this is not a transient disconnect. NC should handle transient network
// errors internally. At this point, NC deems the connection unrecoverable
// and its docs recommend starting the service discovery/advertising process
// again.
LOG(ERROR) << "DataMigration remote device has disconnected unexpectedly";
connected_device_.reset();
// Data Migration protocol does not persist state across connections. Once
// the connection is dropped, the protocol resets. Clear any payloads in
// memory that have not been processed yet since they are guaranteed to not be
// used at this point. Any files that were completely transferred on disc will
// be preserved though.
nearby_connections_manager_->ClearIncomingPayloads();
StartAdvertising();
}
} // namespace data_migration