chromium/chromeos/ash/components/data_migration/data_migration.cc

// 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