blob: b6c6361b7275d5cc4a66464b4f641241fb95746b [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/secure_channel/nearby_connection_broker_impl.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/rand_util.h"
#include "chrome/browser/chromeos/secure_channel/nearby_endpoint_finder.h"
#include "chrome/browser/chromeos/secure_channel/util/histogram_util.h"
#include "chromeos/components/multidevice/logging/logging.h"
namespace chromeos {
namespace secure_channel {
namespace {
using location::nearby::connections::mojom::BytesPayload;
using location::nearby::connections::mojom::ConnectionInfoPtr;
using location::nearby::connections::mojom::ConnectionOptions;
using location::nearby::connections::mojom::DiscoveredEndpointInfoPtr;
using location::nearby::connections::mojom::Medium;
using location::nearby::connections::mojom::MediumSelection;
using location::nearby::connections::mojom::NearbyConnections;
using location::nearby::connections::mojom::Payload;
using location::nearby::connections::mojom::PayloadContent;
using location::nearby::connections::mojom::PayloadPtr;
using location::nearby::connections::mojom::PayloadTransferUpdatePtr;
using location::nearby::connections::mojom::Status;
NearbyConnectionBrokerImpl::Factory* g_test_factory = nullptr;
constexpr base::TimeDelta kConnectionStatusChangeTimeout =
base::TimeDelta::FromSeconds(10);
// Numerical values should not be reused or changed since this is used by
// metrics.
enum class ConnectionMedium {
kConnectedViaBluetooth = 0,
kUpgradedToWebRtc = 1,
kMaxValue = kUpgradedToWebRtc
};
void RecordConnectionMediumMetric(ConnectionMedium medium) {
base::UmaHistogramEnumeration(
"MultiDevice.SecureChannel.Nearby.ConnectionMedium", medium);
}
void RecordWebRtcUpgradeDuration(base::TimeDelta duration) {
// Note: min/max/bucket values should not be changed. If they need to be
// adjusted, a new histogram should be created.
base::UmaHistogramCustomTimes(
"MultiDevice.SecureChannel.Nearby.WebRtcUpgradeDuration", duration,
/*min=*/base::TimeDelta::FromSeconds(1),
/*max=*/base::TimeDelta::FromMinutes(5),
/*buckets=*/50);
}
} // namespace
// static
std::unique_ptr<NearbyConnectionBroker>
NearbyConnectionBrokerImpl::Factory::Create(
const std::vector<uint8_t>& bluetooth_public_address,
NearbyEndpointFinder* endpoint_finder,
mojo::PendingReceiver<mojom::NearbyMessageSender> message_sender_receiver,
mojo::PendingRemote<mojom::NearbyMessageReceiver> message_receiver_remote,
const mojo::SharedRemote<NearbyConnections>& nearby_connections,
base::OnceClosure on_connected_callback,
base::OnceClosure on_disconnected_callback,
std::unique_ptr<base::OneShotTimer> timer) {
if (g_test_factory) {
return g_test_factory->CreateInstance(
bluetooth_public_address, endpoint_finder,
std::move(message_sender_receiver), std::move(message_receiver_remote),
nearby_connections, std::move(on_connected_callback),
std::move(on_disconnected_callback), std::move(timer));
}
return base::WrapUnique(new NearbyConnectionBrokerImpl(
bluetooth_public_address, endpoint_finder,
std::move(message_sender_receiver), std::move(message_receiver_remote),
nearby_connections, std::move(on_connected_callback),
std::move(on_disconnected_callback), std::move(timer)));
}
// static
void NearbyConnectionBrokerImpl::Factory::SetFactoryForTesting(
Factory* test_factory) {
g_test_factory = test_factory;
}
NearbyConnectionBrokerImpl::NearbyConnectionBrokerImpl(
const std::vector<uint8_t>& bluetooth_public_address,
NearbyEndpointFinder* endpoint_finder,
mojo::PendingReceiver<mojom::NearbyMessageSender> message_sender_receiver,
mojo::PendingRemote<mojom::NearbyMessageReceiver> message_receiver_remote,
const mojo::SharedRemote<NearbyConnections>& nearby_connections,
base::OnceClosure on_connected_callback,
base::OnceClosure on_disconnected_callback,
std::unique_ptr<base::OneShotTimer> timer)
: NearbyConnectionBroker(bluetooth_public_address,
std::move(message_sender_receiver),
std::move(message_receiver_remote),
std::move(on_connected_callback),
std::move(on_disconnected_callback)),
endpoint_finder_(endpoint_finder),
nearby_connections_(nearby_connections),
timer_(std::move(timer)) {
TransitionToStatus(ConnectionStatus::kDiscoveringEndpoint);
endpoint_finder_->FindEndpoint(
bluetooth_public_address,
base::BindOnce(&NearbyConnectionBrokerImpl::OnEndpointDiscovered,
base::Unretained(this)),
base::BindOnce(&NearbyConnectionBrokerImpl::OnDiscoveryFailure,
base::Unretained(this)));
}
NearbyConnectionBrokerImpl::~NearbyConnectionBrokerImpl() = default;
void NearbyConnectionBrokerImpl::TransitionToStatus(
ConnectionStatus connection_status) {
PA_LOG(INFO) << "Nearby Connection status: " << connection_status_ << " => "
<< connection_status;
connection_status_ = connection_status;
timer_->Stop();
// The connected and disconnected states do not expect any further state
// changes.
if (connection_status_ == ConnectionStatus::kConnected ||
connection_status_ == ConnectionStatus::kDisconnected) {
return;
}
// If the state does not change within |kConnectionStatusChangeTimeout|, time
// out and give up on the connection.
timer_->Start(
FROM_HERE, kConnectionStatusChangeTimeout,
base::BindOnce(
&NearbyConnectionBrokerImpl::OnConnectionStatusChangeTimeout,
weak_ptr_factory_.GetWeakPtr()));
}
void NearbyConnectionBrokerImpl::Disconnect(
util::NearbyDisconnectionReason reason) {
// Only log a single disconnection reason per connection attempt. Edge cases
// can cause this function to be invoked multiple times.
if (!has_disconnect_reason_been_logged_) {
has_disconnect_reason_been_logged_ = true;
util::RecordNearbyDisconnection(reason);
}
if (!need_to_disconnect_endpoint_) {
TransitionToDisconnectedAndInvokeCallback();
return;
}
if (connection_status_ == ConnectionStatus::kDisconnecting)
return;
TransitionToStatus(ConnectionStatus::kDisconnecting);
nearby_connections_->DisconnectFromEndpoint(
mojom::kServiceId, remote_endpoint_id_,
base::BindOnce(
&NearbyConnectionBrokerImpl::OnDisconnectFromEndpointResult,
weak_ptr_factory_.GetWeakPtr()));
}
void NearbyConnectionBrokerImpl::TransitionToDisconnectedAndInvokeCallback() {
if (connection_status_ == ConnectionStatus::kDisconnected)
return;
TransitionToStatus(ConnectionStatus::kDisconnected);
InvokeDisconnectedCallback();
}
void NearbyConnectionBrokerImpl::OnEndpointDiscovered(
const std::string& endpoint_id,
DiscoveredEndpointInfoPtr info) {
DCHECK_EQ(ConnectionStatus::kDiscoveringEndpoint, connection_status_);
DCHECK(!endpoint_id.empty());
remote_endpoint_id_ = endpoint_id;
TransitionToStatus(ConnectionStatus::kRequestingConnection);
nearby_connections_->RequestConnection(
mojom::kServiceId, info->endpoint_info, remote_endpoint_id_,
ConnectionOptions::New(MediumSelection::New(/*bluetooth=*/true,
/*ble=*/false,
/*webrtc=*/true,
/*wifi_lan=*/false),
/*remote_bluetooth_mac_address=*/base::nullopt),
connection_lifecycle_listener_receiver_.BindNewPipeAndPassRemote(),
base::BindOnce(&NearbyConnectionBrokerImpl::OnRequestConnectionResult,
weak_ptr_factory_.GetWeakPtr()));
}
void NearbyConnectionBrokerImpl::OnDiscoveryFailure() {
DCHECK_EQ(ConnectionStatus::kDiscoveringEndpoint, connection_status_);
Disconnect(util::NearbyDisconnectionReason::kFailedDiscovery);
}
void NearbyConnectionBrokerImpl::OnRequestConnectionResult(Status status) {
// In the success case, OnConnectionInitiated() is expected to be called to
// continue the flow, so nothing else needs to be done in this callback.
if (status == Status::kSuccess)
return;
PA_LOG(WARNING) << "RequestConnection() failed: " << status;
Disconnect(util::NearbyDisconnectionReason::kFailedRequestingConnection);
}
void NearbyConnectionBrokerImpl::OnAcceptConnectionResult(Status status) {
if (status == Status::kSuccess) {
DCHECK_EQ(ConnectionStatus::kAcceptingConnection, connection_status_);
TransitionToStatus(
ConnectionStatus::kWaitingForConnectionToBeAcceptedByRemoteDevice);
return;
}
PA_LOG(WARNING) << "AcceptConnection() failed: " << status;
Disconnect(util::NearbyDisconnectionReason::kFailedAcceptingConnection);
}
void NearbyConnectionBrokerImpl::OnSendPayloadResult(
SendMessageCallback callback,
Status status) {
bool success = status == Status::kSuccess;
std::move(callback).Run(success);
base::UmaHistogramBoolean(
"MultiDevice.SecureChannel.Nearby.SendMessageResult", success);
if (success)
return;
PA_LOG(WARNING) << "OnSendPayloadResult() failed: " << status;
Disconnect(util::NearbyDisconnectionReason::kSendMessageFailed);
}
void NearbyConnectionBrokerImpl::OnDisconnectFromEndpointResult(Status status) {
// If the disconnection was successful, wait for the OnDisconnected()
// callback.
if (status == Status::kSuccess)
return;
PA_LOG(WARNING) << "Failed to disconnect from endpoint with ID "
<< remote_endpoint_id_ << ": " << status;
need_to_disconnect_endpoint_ = false;
Disconnect(util::NearbyDisconnectionReason::kDisconnectionRequestedByClient);
}
void NearbyConnectionBrokerImpl::OnConnectionStatusChangeTimeout() {
if (connection_status_ == ConnectionStatus::kDisconnecting) {
PA_LOG(WARNING) << "Timeout disconnecting from endpoint";
TransitionToDisconnectedAndInvokeCallback();
return;
}
// If there is a timeout requesting a connection, we should still try to
// disconnect from the endpoint in case the endpoint was almost about to be
// connected before the timeout occurred.
if (connection_status_ == ConnectionStatus::kRequestingConnection)
need_to_disconnect_endpoint_ = true;
PA_LOG(WARNING) << "Timeout changing connection status";
util::NearbyDisconnectionReason reason;
switch (connection_status_) {
case ConnectionStatus::kDiscoveringEndpoint:
reason = util::NearbyDisconnectionReason::kTimeoutDuringDiscovery;
break;
case ConnectionStatus::kRequestingConnection:
reason = util::NearbyDisconnectionReason::kTimeoutDuringRequestConnection;
break;
case ConnectionStatus::kAcceptingConnection:
reason = util::NearbyDisconnectionReason::kTimeoutDuringAcceptConnection;
break;
case ConnectionStatus::kWaitingForConnectionToBeAcceptedByRemoteDevice:
reason =
util::NearbyDisconnectionReason::kTimeoutWaitingForConnectionAccepted;
break;
default:
NOTREACHED() << "Unexpected timeout with connection status "
<< connection_status_;
reason = util::NearbyDisconnectionReason::kConnectionLost;
break;
}
Disconnect(reason);
}
void NearbyConnectionBrokerImpl::OnMojoDisconnection() {
Disconnect(util::NearbyDisconnectionReason::kDisconnectionRequestedByClient);
}
void NearbyConnectionBrokerImpl::SendMessage(const std::string& message,
SendMessageCallback callback) {
DCHECK_EQ(ConnectionStatus::kConnected, connection_status_);
std::vector<uint8_t> message_as_bytes(message.begin(), message.end());
// Randomly generate a new payload ID for each message sent. Each payload is
// expected to have its own ID, so we randomly generate one each time instead
// of starting from 0 for each NearbyConnectionBrokerImpl instance. Note that
// payloads are only shared between two devices, so the chance of a collision
// in a 64-bit value is negligible.
uint64_t unsigned_payload_id = base::RandUint64();
// Interpret |unsigned_payload_id|'s bytes as a signed value for use in the
// SendPayload() API.
const int64_t* payload_id_ptr =
reinterpret_cast<const int64_t*>(&unsigned_payload_id);
nearby_connections_->SendPayload(
mojom::kServiceId, std::vector<std::string>{remote_endpoint_id_},
Payload::New(*payload_id_ptr, PayloadContent::NewBytes(
BytesPayload::New(message_as_bytes))),
base::BindOnce(&NearbyConnectionBrokerImpl::OnSendPayloadResult,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
util::LogMessageAction(util::MessageAction::kMessageSent);
}
void NearbyConnectionBrokerImpl::OnConnectionInitiated(
const std::string& endpoint_id,
ConnectionInfoPtr info) {
if (remote_endpoint_id_ != endpoint_id) {
PA_LOG(WARNING) << "OnConnectionInitiated(): unexpected endpoint ID "
<< endpoint_id;
return;
}
DCHECK_EQ(ConnectionStatus::kRequestingConnection, connection_status_);
TransitionToStatus(ConnectionStatus::kAcceptingConnection);
need_to_disconnect_endpoint_ = true;
nearby_connections_->AcceptConnection(
mojom::kServiceId, remote_endpoint_id_,
payload_listener_receiver_.BindNewPipeAndPassRemote(),
base::BindOnce(&NearbyConnectionBrokerImpl::OnAcceptConnectionResult,
weak_ptr_factory_.GetWeakPtr()));
}
void NearbyConnectionBrokerImpl::OnConnectionAccepted(
const std::string& endpoint_id) {
if (remote_endpoint_id_ != endpoint_id) {
PA_LOG(WARNING) << "OnConnectionAccepted(): unexpected endpoint ID "
<< endpoint_id;
return;
}
DCHECK_EQ(ConnectionStatus::kWaitingForConnectionToBeAcceptedByRemoteDevice,
connection_status_);
TransitionToStatus(ConnectionStatus::kConnected);
RecordConnectionMediumMetric(ConnectionMedium::kConnectedViaBluetooth);
time_when_connection_accepted_ = base::Time::Now();
NotifyConnected();
}
void NearbyConnectionBrokerImpl::OnConnectionRejected(
const std::string& endpoint_id,
Status status) {
if (remote_endpoint_id_ != endpoint_id) {
PA_LOG(WARNING) << "OnConnectionRejected(): unexpected endpoint ID "
<< endpoint_id;
return;
}
PA_LOG(WARNING) << "Connection rejected: " << status;
Disconnect(util::NearbyDisconnectionReason::kConnectionRejected);
}
void NearbyConnectionBrokerImpl::OnDisconnected(
const std::string& endpoint_id) {
if (remote_endpoint_id_ != endpoint_id) {
PA_LOG(WARNING) << "OnDisconnected(): unexpected endpoint ID "
<< endpoint_id;
return;
}
if (connection_status_ != ConnectionStatus::kDisconnecting) {
PA_LOG(WARNING) << "Connection disconnected unexpectedly";
}
need_to_disconnect_endpoint_ = false;
Disconnect(util::NearbyDisconnectionReason::kConnectionLost);
}
void NearbyConnectionBrokerImpl::OnBandwidthChanged(
const std::string& endpoint_id,
Medium medium) {
if (remote_endpoint_id_ != endpoint_id) {
PA_LOG(WARNING) << "OnBandwidthChanged(): unexpected endpoint ID "
<< endpoint_id;
return;
}
PA_LOG(INFO) << "Bandwidth changed: " << medium;
if (medium == Medium::kWebRtc) {
RecordConnectionMediumMetric(ConnectionMedium::kUpgradedToWebRtc);
DCHECK(!time_when_connection_accepted_.is_null());
base::TimeDelta webrtc_upgrade_duration =
base::Time::Now() - time_when_connection_accepted_;
RecordWebRtcUpgradeDuration(webrtc_upgrade_duration);
}
}
void NearbyConnectionBrokerImpl::OnPayloadReceived(
const std::string& endpoint_id,
PayloadPtr payload) {
if (remote_endpoint_id_ != endpoint_id) {
PA_LOG(WARNING) << "OnPayloadReceived(): unexpected endpoint ID "
<< endpoint_id;
return;
}
if (!payload->content->is_bytes()) {
PA_LOG(WARNING) << "OnPayloadReceived(): Received unexpected payload type "
<< "(was expecting bytes type). Disconnecting.";
Disconnect(util::NearbyDisconnectionReason::kReceivedUnexpectedPayloadType);
return;
}
PA_LOG(VERBOSE) << "OnPayloadReceived(): Received message with payload ID "
<< payload->id;
const std::vector<uint8_t>& message_as_bytes =
payload->content->get_bytes()->bytes;
NotifyMessageReceived(
std::string(message_as_bytes.begin(), message_as_bytes.end()));
util::LogMessageAction(util::MessageAction::kMessageReceived);
}
std::ostream& operator<<(std::ostream& stream,
NearbyConnectionBrokerImpl::ConnectionStatus status) {
switch (status) {
case NearbyConnectionBrokerImpl::ConnectionStatus::kUninitialized:
stream << "[Uninitialized]";
break;
case NearbyConnectionBrokerImpl::ConnectionStatus::kDiscoveringEndpoint:
stream << "[Discovering endpoint]";
break;
case NearbyConnectionBrokerImpl::ConnectionStatus::kRequestingConnection:
stream << "[Requesting connection]";
break;
case NearbyConnectionBrokerImpl::ConnectionStatus::kAcceptingConnection:
stream << "[Accepting connection]";
break;
case NearbyConnectionBrokerImpl::ConnectionStatus::
kWaitingForConnectionToBeAcceptedByRemoteDevice:
stream << "[Waiting for connection to be accepted]";
break;
case NearbyConnectionBrokerImpl::ConnectionStatus::kConnected:
stream << "[Connected]";
break;
case NearbyConnectionBrokerImpl::ConnectionStatus::kDisconnecting:
stream << "[Disconnecting]";
break;
case NearbyConnectionBrokerImpl::ConnectionStatus::kDisconnected:
stream << "[Disconnected]";
break;
}
return stream;
}
} // namespace secure_channel
} // namespace chromeos