// 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/services/sharing/nearby/platform/bluetooth_classic_medium.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/metrics/histogram_functions.h"
#include "chrome/services/sharing/nearby/platform/bluetooth_server_socket.h"
#include "chrome/services/sharing/nearby/platform/bluetooth_socket.h"
#include "components/cross_device/nearby/nearby_features.h"
#include "device/bluetooth/public/cpp/bluetooth_uuid.h"
namespace nearby {
namespace chrome {
namespace {
// Client name for logging in BLE scanning.
constexpr char kScanClientName[] = "Nearby Connections";
// Duration of time after which inactive Bluetooth devices may be removed from
// the discovered devices map.
const base::TimeDelta kStaleBluetoothDeviceTimeout = base::Seconds(20);
void LogStartDiscoveryResult(bool success) {
base::UmaHistogramBoolean(
"Nearby.Connections.Bluetooth.ClassicMedium.StartDiscovery.Result",
success);
}
void LogStopDiscoveryResult(bool success) {
base::UmaHistogramBoolean(
"Nearby.Connections.Bluetooth.ClassicMedium.StopDiscovery.Result",
success);
}
void LogConnectToServiceResult(bool success) {
base::UmaHistogramBoolean(
"Nearby.Connections.Bluetooth.ClassicMedium.ConnectToService.Result",
success);
}
void LogConnectToServiceDuration(base::TimeDelta duration) {
base::UmaHistogramTimes(
"Nearby.Connections.Bluetooth.ClassicMedium.ConnectToService.Duration",
duration);
}
void LogListenForServiceResult(bool success) {
base::UmaHistogramBoolean(
"Nearby.Connections.Bluetooth.ClassicMedium.ListenForService.Result",
success);
}
} // namespace
BluetoothClassicMedium::BluetoothClassicMedium(
const mojo::SharedRemote<bluetooth::mojom::Adapter>& adapter)
: adapter_(adapter),
stale_bluetooth_device_timer_(
FROM_HERE,
kStaleBluetoothDeviceTimeout / 4,
base::BindRepeating(
&BluetoothClassicMedium::RemoveStaleBluetoothDevices,
base::Unretained(this))) {
DCHECK(adapter_.is_bound());
}
BluetoothClassicMedium::~BluetoothClassicMedium() = default;
bool BluetoothClassicMedium::StartDiscovery(
DiscoveryCallback discovery_callback) {
if (!features::IsNearbyBluetoothClassicScanningEnabled()) {
VLOG(1) << ": Classic scanning disabled, failing to StartDiscovery for BT "
"Classic";
return false;
}
if (adapter_observer_.is_bound() && discovery_callback_ &&
discovery_session_.is_bound()) {
LogStartDiscoveryResult(true);
return true;
}
// TODO(hansberry): Verify with Nearby team if this is correct behavior.
discovered_bluetooth_devices_map_.clear();
bool success =
adapter_->AddObserver(adapter_observer_.BindNewPipeAndPassRemote());
if (!success) {
adapter_observer_.reset();
LogStartDiscoveryResult(false);
return false;
}
mojo::PendingRemote<bluetooth::mojom::DiscoverySession> discovery_session;
success =
adapter_->StartDiscoverySession(kScanClientName, &discovery_session);
if (!success || !discovery_session.is_valid()) {
adapter_observer_.reset();
LogStartDiscoveryResult(false);
return false;
}
discovery_session_.Bind(std::move(discovery_session));
discovery_session_.set_disconnect_handler(
base::BindOnce(&BluetoothClassicMedium::DiscoveringChanged,
base::Unretained(this), /*discovering=*/false));
discovery_callback_ = std::move(discovery_callback);
stale_bluetooth_device_timer_.Reset();
LogStartDiscoveryResult(true);
return true;
}
bool BluetoothClassicMedium::StopDiscovery() {
// TODO(hansberry): Verify with Nearby team if this is correct behavior:
// Do not clear |discovered_bluetooth_devices_map_| because the caller still
// needs references to BluetoothDevices to remain valid.
bool stop_discovery_success = true;
if (discovery_session_) {
bool message_success = discovery_session_->Stop(&stop_discovery_success);
stop_discovery_success = stop_discovery_success && message_success;
}
adapter_observer_.reset();
discovery_callback_.reset();
discovery_session_.reset();
stale_bluetooth_device_timer_.Stop();
LogStopDiscoveryResult(stop_discovery_success);
return stop_discovery_success;
}
std::unique_ptr<api::BluetoothSocket> BluetoothClassicMedium::ConnectToService(
api::BluetoothDevice& remote_device,
const std::string& service_uuid,
CancellationFlag* cancellation_flag) {
if (cancellation_flag && cancellation_flag->Cancelled()) {
return nullptr;
}
const std::string& address = remote_device.GetMacAddress();
auto start_time = base::TimeTicks::Now();
bluetooth::mojom::ConnectToServiceResultPtr result;
bool success = adapter_->ConnectToServiceInsecurely(
address, device::BluetoothUUID(service_uuid),
/*should_unbond_on_error=*/true, &result);
if (success && result) {
LogConnectToServiceDuration(base::TimeTicks::Now() - start_time);
LogConnectToServiceResult(true);
return std::make_unique<chrome::BluetoothSocket>(
remote_device, std::move(result->socket),
std::move(result->receive_stream), std::move(result->send_stream));
}
LogConnectToServiceResult(false);
return nullptr;
}
std::unique_ptr<api::BluetoothServerSocket>
BluetoothClassicMedium::ListenForService(const std::string& service_name,
const std::string& service_uuid) {
mojo::PendingRemote<bluetooth::mojom::ServerSocket> server_socket;
bool success = adapter_->CreateRfcommServiceInsecurely(
service_name, device::BluetoothUUID(service_uuid), &server_socket);
if (success && server_socket) {
LogListenForServiceResult(true);
return std::make_unique<chrome::BluetoothServerSocket>(
std::move(server_socket));
}
LogListenForServiceResult(false);
return nullptr;
}
std::unique_ptr<api::BluetoothPairing>
BluetoothClassicMedium::CreatePairing(api::BluetoothDevice& remote_device) {
// TODO(b/280656073): Add Chromium implementation for BluetoothPairing.
NOTIMPLEMENTED();
return nullptr;
}
BluetoothDevice* BluetoothClassicMedium::GetRemoteDevice(
const std::string& mac_address) {
auto it = discovered_bluetooth_devices_map_.find(mac_address);
if (it != discovered_bluetooth_devices_map_.end())
return &it->second;
// If a device with |mac_address| has not been found, Nearby Connections
// is attempting to connect to a device with |mac_adress| which is not
// discoverable. Create a placeholder BluetoothDevice to be used by
// ConnectToService().
bluetooth::mojom::DeviceInfoPtr device = bluetooth::mojom::DeviceInfo::New();
device->address = mac_address;
return &discovered_bluetooth_devices_map_
.emplace(mac_address, std::move(device))
.first->second;
}
void BluetoothClassicMedium::PresentChanged(bool present) {
// TODO(hansberry): It is unclear to me how the API implementation can signal
// to Core that |present| has become unexpectedly false. Need to ask
// Nearby team.
if (!present)
StopDiscovery();
}
void BluetoothClassicMedium::PoweredChanged(bool powered) {
// TODO(hansberry): It is unclear to me how the API implementation can signal
// to Core that |powered| has become unexpectedly false. Need to ask
// Nearby team.
if (!powered)
StopDiscovery();
}
void BluetoothClassicMedium::DiscoverableChanged(bool discoverable) {
// Do nothing. BluetoothClassicMedium is not responsible for managing
// discoverable state.
NOTIMPLEMENTED();
}
void BluetoothClassicMedium::DiscoveringChanged(bool discovering) {
// TODO(hansberry): It is unclear to me how the API implementation can signal
// to Core that |discovering| has become unexpectedly false. Need to ask
// Nearby team.
if (!discovering) {
StopDiscovery();
}
}
void BluetoothClassicMedium::DeviceAdded(
bluetooth::mojom::DeviceInfoPtr device) {
if (!adapter_observer_.is_bound() || !discovery_callback_ ||
!discovery_session_.is_bound()) {
return;
}
// Best-effort attempt to filter out BLE advertisements. BLE advertisements
// represented as "devices" may have their |name| set if the system has
// created a GATT connection to the advertiser, but all BT Classic devices
// that we are interested in must have their |name| set. See BleMedium
// for separate discovery of BLE advertisements (BlePeripherals).
if (!device->name) {
return;
}
const std::string& address = device->address;
if (base::Contains(discovered_bluetooth_devices_map_, address)) {
auto& bluetooth_device = discovered_bluetooth_devices_map_.at(address);
bool name_changed = device->name.has_value() &&
device->name.value() != bluetooth_device.GetName();
bluetooth_device.UpdateDevice(std::move(device), base::TimeTicks::Now());
if (name_changed) {
discovery_callback_->device_name_changed_cb(bluetooth_device);
}
} else {
discovered_bluetooth_devices_map_.emplace(
std::piecewise_construct, std::make_tuple(address),
std::make_tuple(std::move(device), base::TimeTicks::Now()));
discovery_callback_->device_discovered_cb(
discovered_bluetooth_devices_map_.at(address));
}
}
void BluetoothClassicMedium::DeviceChanged(
bluetooth::mojom::DeviceInfoPtr device) {
DeviceAdded(std::move(device));
}
void BluetoothClassicMedium::DeviceRemoved(
bluetooth::mojom::DeviceInfoPtr device) {
if (!adapter_observer_.is_bound() || !discovery_callback_ ||
!discovery_session_.is_bound()) {
return;
}
const std::string& address = device->address;
if (!base::Contains(discovered_bluetooth_devices_map_, address))
return;
discovery_callback_->device_lost_cb(
discovered_bluetooth_devices_map_.at(address));
discovered_bluetooth_devices_map_.erase(address);
}
void BluetoothClassicMedium::RemoveStaleBluetoothDevices() {
base::TimeTicks earliest_acceptable_discovery_time =
base::TimeTicks::Now() - kStaleBluetoothDeviceTimeout;
auto it = discovered_bluetooth_devices_map_.begin();
while (it != discovered_bluetooth_devices_map_.end()) {
if (it->second.GetLastDiscoveredTime().has_value() &&
it->second.GetLastDiscoveredTime().value() <
earliest_acceptable_discovery_time) {
if (discovery_callback_) {
discovery_callback_->device_lost_cb(it->second);
}
it = discovered_bluetooth_devices_map_.erase(it);
} else {
++it;
}
}
}
} // namespace chrome
} // namespace nearby