// 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/browser/ash/secure_channel/nearby_connection_broker_impl.h"
#include <memory>
#include <utility>
#include "ash/constants/ash_features.h"
#include "base/containers/flat_map.h"
#include "base/files/file.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/rand_util.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "chrome/browser/ash/secure_channel/nearby_endpoint_finder.h"
#include "chrome/browser/ash/secure_channel/util/histogram_util.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/services/secure_channel/public/mojom/nearby_connector.mojom-shared.h"
#include "chromeos/ash/services/secure_channel/public/mojom/secure_channel_types.mojom.h"
#include "mojo/public/cpp/bindings/remote.h"
namespace ash {
namespace secure_channel {
namespace {
using ::nearby::connections::mojom::BytesPayload;
using ::nearby::connections::mojom::ConnectionInfoPtr;
using ::nearby::connections::mojom::ConnectionOptions;
using ::nearby::connections::mojom::DiscoveredEndpointInfoPtr;
using ::nearby::connections::mojom::Medium;
using ::nearby::connections::mojom::MediumSelection;
using ::nearby::connections::mojom::NearbyConnections;
using ::nearby::connections::mojom::Payload;
using ::nearby::connections::mojom::PayloadContent;
using ::nearby::connections::mojom::PayloadPtr;
using ::nearby::connections::mojom::PayloadStatus;
using ::nearby::connections::mojom::PayloadTransferUpdatePtr;
using ::nearby::connections::mojom::Status;
NearbyConnectionBrokerImpl::Factory* g_test_factory = nullptr;
constexpr base::TimeDelta kConnectionStatusChangeTimeout = base::Seconds(10);
// The amount of time by which we can expect a WebRTC upgrade to have been
// completed. According to metrics, 30 seconds is the 95th+ percentile of how
// long it takes to upgrade to WebRTC.
constexpr base::TimeDelta kWebRtcUpgradeDelay = base::Seconds(30);
// These values are set to help with Phone Hub battery drain (see: b/183505430)
// by making the Nearby Connections layer 'keep alive' ping and activity timeout
// longer. There are additional values at the WebRTC peer connection layer that
// are independent of these values and both must also be set longer than the
// defaults for battery life to improve. These two layers (Nearby Connections
// and WebRtc) do not have to be synced explicitly, but the shortest interval
// will drive the number of kernel wakeups that cause the battery drain on the
// phone side. Both layers produce their own pings/keep alive messages so Nearby
// Connections is not responsible for sending data to keep the WebRTC layer
// alive. If these values need to be tweaked, make sure to run a power analysis
// for Phone battery drain with a persistent Phone Hub connection and understand
// the impact.
//
// Nearby Connections keep alive interval default is 5 seconds.
constexpr base::TimeDelta kKeepAliveInterval = base::Seconds(25);
// Nearby Connections keep alive timeout default is 30 seconds. When the phone
// goes into deep sleep mode Chrome OS cannot expect to receive keepalives every
// 25 seconds. The timeout needs to be set high enough to ensure a stable
// connection when the phone is in deep sleep mode.
constexpr base::TimeDelta kKeepAliveTimeout = base::Minutes(10);
// Numerical values should not be reused or changed since this is used by
// metrics.
enum class ConnectionMedium {
kConnectedViaBluetooth = 0,
kUpgradedToWebRtc = 1,
kDisconnectedInUnder30Seconds = 2,
kMaxValue = kDisconnectedInUnder30Seconds
};
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::Seconds(1),
/*max=*/base::Minutes(5),
/*buckets=*/50);
}
scoped_refptr<base::SequencedTaskRunner> CreateTaskRunner() {
return base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_VISIBLE});
}
mojom::NearbyConnectionStepResult ConvertStatusToStepResult(Status status) {
switch (status) {
case Status::kError:
return mojom::NearbyConnectionStepResult::kError;
case Status::kOutOfOrderApiCall:
return mojom::NearbyConnectionStepResult::kOutOfOrderApiCall;
case Status::kAlreadyHaveActiveStrategy:
return mojom::NearbyConnectionStepResult::kAlreadyHaveActiveStrategy;
case Status::kAlreadyAdvertising:
return mojom::NearbyConnectionStepResult::kAlreadyAdvertising;
case Status::kAlreadyDiscovering:
return mojom::NearbyConnectionStepResult::kAlreadyDiscovering;
case Status::kEndpointIOError:
return mojom::NearbyConnectionStepResult::kEndpointIOError;
case Status::kEndpointUnknown:
return mojom::NearbyConnectionStepResult::kEndpointUnknown;
case Status::kConnectionRejected:
return mojom::NearbyConnectionStepResult::kConnectionRejected;
case Status::kAlreadyConnectedToEndpoint:
return mojom::NearbyConnectionStepResult::kAlreadyConnectedToEndpoint;
case Status::kNotConnectedToEndpoint:
return mojom::NearbyConnectionStepResult::kNotConnectedToEndpoint;
case Status::kBluetoothError:
return mojom::NearbyConnectionStepResult::kBluetoothError;
case Status::kBleError:
return mojom::NearbyConnectionStepResult::kBleError;
case Status::kWifiLanError:
return mojom::NearbyConnectionStepResult::kWifiLanError;
case Status::kPayloadUnknown:
return mojom::NearbyConnectionStepResult::kPayloadUnknown;
case Status::kAlreadyListening:
return mojom::NearbyConnectionStepResult::kAlreadyAdvertising;
case Status::kReset:
return mojom::NearbyConnectionStepResult::kReset;
case Status::kTimeout:
return mojom::NearbyConnectionStepResult::kTimeout;
case Status::kUnknown:
return mojom::NearbyConnectionStepResult::kUnknown;
case Status::kSuccess:
return mojom::NearbyConnectionStepResult::kSuccess;
case Status::kNextValue:
NOTREACHED_IN_MIGRATION();
return mojom::NearbyConnectionStepResult::kMaxValue;
}
}
} // namespace
// static
std::unique_ptr<NearbyConnectionBroker>
NearbyConnectionBrokerImpl::Factory::Create(
const std::vector<uint8_t>& bluetooth_public_address,
const std::vector<uint8_t>& eid,
NearbyEndpointFinder* endpoint_finder,
mojo::PendingReceiver<mojom::NearbyMessageSender> message_sender_receiver,
mojo::PendingReceiver<mojom::NearbyFilePayloadHandler>
file_payload_handler_receiver,
mojo::PendingRemote<mojom::NearbyMessageReceiver> message_receiver_remote,
mojo::PendingRemote<mojom::NearbyConnectionStateListener>
nearby_connection_state_listener,
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(file_payload_handler_receiver),
std::move(message_receiver_remote),
std::move(nearby_connection_state_listener), nearby_connections,
std::move(on_connected_callback), std::move(on_disconnected_callback),
std::move(timer));
}
return base::WrapUnique(new NearbyConnectionBrokerImpl(
bluetooth_public_address, eid, endpoint_finder,
std::move(message_sender_receiver),
std::move(file_payload_handler_receiver),
std::move(message_receiver_remote),
std::move(nearby_connection_state_listener), 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,
const std::vector<uint8_t>& eid,
NearbyEndpointFinder* endpoint_finder,
mojo::PendingReceiver<mojom::NearbyMessageSender> message_sender_receiver,
mojo::PendingReceiver<mojom::NearbyFilePayloadHandler>
file_payload_handler_receiver,
mojo::PendingRemote<mojom::NearbyMessageReceiver> message_receiver_remote,
mojo::PendingRemote<mojom::NearbyConnectionStateListener>
nearby_connection_state_listener,
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(file_payload_handler_receiver),
std::move(message_receiver_remote),
std::move(nearby_connection_state_listener),
std::move(on_connected_callback),
std::move(on_disconnected_callback)),
endpoint_finder_(endpoint_finder),
nearby_connections_(nearby_connections),
timer_(std::move(timer)),
task_runner_(CreateTaskRunner()) {
TransitionToStatus(ConnectionStatus::kDiscoveringEndpoint);
NotifyConnectionStateChanged(
mojom::NearbyConnectionStep::kDiscoveringEndpointStarted,
mojom::NearbyConnectionStepResult::kSuccess);
endpoint_finder_->FindEndpoint(
bluetooth_public_address, eid,
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 (!has_recorded_no_webrtc_metric_ && !has_upgraded_to_webrtc_ &&
!time_when_connection_accepted_.is_null() &&
(base::Time::Now() - time_when_connection_accepted_) <
kWebRtcUpgradeDelay) {
has_recorded_no_webrtc_metric_ = true;
RecordConnectionMediumMetric(
ConnectionMedium::kDisconnectedInUnder30Seconds);
}
if (!need_to_disconnect_endpoint_) {
TransitionToDisconnectedAndInvokeCallback();
return;
}
if (connection_status_ == ConnectionStatus::kDisconnecting) {
return;
}
TransitionToStatus(ConnectionStatus::kDisconnecting);
NotifyConnectionStateChanged(
mojom::NearbyConnectionStep::kDisconnectionStarted,
mojom::NearbyConnectionStepResult::kSuccess);
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);
NotifyConnectionStateChanged(
mojom::NearbyConnectionStep::kDisconnectionFinished,
mojom::NearbyConnectionStepResult::kSuccess);
CleanUpPendingFileTransfers();
InvokeDisconnectedCallback();
}
void NearbyConnectionBrokerImpl::OnEndpointDiscovered(
const std::string& endpoint_id,
DiscoveredEndpointInfoPtr info) {
DCHECK_EQ(ConnectionStatus::kDiscoveringEndpoint, connection_status_);
NotifyConnectionStateChanged(
mojom::NearbyConnectionStep::kDiscoveringEndpointEnded,
mojom::NearbyConnectionStepResult::kSuccess);
DCHECK(!endpoint_id.empty());
remote_endpoint_id_ = endpoint_id;
TransitionToStatus(ConnectionStatus::kRequestingConnection);
NotifyConnectionStateChanged(
mojom::NearbyConnectionStep::kRequestingConnectionStarted,
mojom::NearbyConnectionStepResult::kSuccess);
nearby_connections_->RequestConnection(
mojom::kServiceId, info->endpoint_info, remote_endpoint_id_,
ConnectionOptions::New(MediumSelection::New(/*bluetooth=*/true,
/*ble=*/false,
/*webrtc=*/true,
/*wifi_lan=*/false,
/*wifi_direct=*/false),
/*remote_bluetooth_mac_address=*/std::nullopt,
features::IsNearbyKeepAliveFixEnabled()
? std::make_optional(kKeepAliveInterval)
: std::nullopt,
features::IsNearbyKeepAliveFixEnabled()
? std::make_optional(kKeepAliveTimeout)
: std::nullopt),
connection_lifecycle_listener_receiver_.BindNewPipeAndPassRemote(),
base::BindOnce(&NearbyConnectionBrokerImpl::OnRequestConnectionResult,
weak_ptr_factory_.GetWeakPtr()));
}
void NearbyConnectionBrokerImpl::OnDiscoveryFailure(Status status) {
DCHECK_EQ(ConnectionStatus::kDiscoveringEndpoint, connection_status_);
NotifyConnectionStateChanged(
mojom::NearbyConnectionStep::kDiscoveringEndpointEnded,
ConvertStatusToStepResult(status));
Disconnect(util::NearbyDisconnectionReason::kFailedDiscovery);
}
void NearbyConnectionBrokerImpl::OnRequestConnectionResult(Status status) {
util::RecordRequestConnectionResult(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) {
NotifyConnectionStateChanged(
mojom::NearbyConnectionStep::kRequestingConnectionEnded,
mojom::NearbyConnectionStepResult::kSuccess);
return;
}
PA_LOG(WARNING) << "RequestConnection() failed: " << status;
if (connection_status_ != ConnectionStatus::kDisconnecting) {
// OnDiscoveryFailure maybe invoked after OnConnectionStatusChangeTimeout.
// In this case, we have already recorded NearbyConnectionStep as failed due
// to timeout.
NotifyConnectionStateChanged(
mojom::NearbyConnectionStep::kRequestingConnectionEnded,
ConvertStatusToStepResult(status));
}
Disconnect(util::NearbyDisconnectionReason::kFailedRequestingConnection);
}
void NearbyConnectionBrokerImpl::OnAcceptConnectionResult(Status status) {
util::RecordAcceptConnectionResult(status);
if (status == Status::kSuccess) {
// It is possible that by the time OnAcceptConnectionResult() is invoked,
// we have already passed the kAcceptingConnection (e.g., if the connection
// was already accepted). To ensure we don't accidentally disconnect from a
// valid connection, only transition to
// kWaitingForConnectionToBeAcceptedByRemoteDevice if we are still accepting
// the connection. See https://crbug.com/1175489 for details.
if (connection_status_ == ConnectionStatus::kAcceptingConnection) {
NotifyConnectionStateChanged(
mojom::NearbyConnectionStep::kAcceptingConnectionFinished,
mojom::NearbyConnectionStepResult::kSuccess);
TransitionToStatus(
ConnectionStatus::kWaitingForConnectionToBeAcceptedByRemoteDevice);
NotifyConnectionStateChanged(
mojom::NearbyConnectionStep::
kWaitingForConnectionToBeAcceptedByRemoteDeviceStarted,
mojom::NearbyConnectionStepResult::kSuccess);
}
return;
}
PA_LOG(WARNING) << "AcceptConnection() failed: " << status;
if (connection_status_ == ConnectionStatus::kAcceptingConnection) {
NotifyConnectionStateChanged(
mojom::NearbyConnectionStep::kAcceptingConnectionFinished,
ConvertStatusToStepResult(status));
}
Disconnect(util::NearbyDisconnectionReason::kFailedAcceptingConnection);
}
void NearbyConnectionBrokerImpl::OnSendPayloadResult(
SendMessageCallback callback,
Status status) {
util::RecordSendPayloadResult(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) {
util::RecordDisconnectFromEndpointResult(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;
mojom::NearbyConnectionStep connection_step;
switch (connection_status_) {
case ConnectionStatus::kDiscoveringEndpoint:
reason = util::NearbyDisconnectionReason::kTimeoutDuringDiscovery;
connection_step = mojom::NearbyConnectionStep::kDiscoveringEndpointEnded;
break;
case ConnectionStatus::kRequestingConnection:
reason = util::NearbyDisconnectionReason::kTimeoutDuringRequestConnection;
connection_step = mojom::NearbyConnectionStep::kRequestingConnectionEnded;
break;
case ConnectionStatus::kAcceptingConnection:
reason = util::NearbyDisconnectionReason::kTimeoutDuringAcceptConnection;
connection_step =
mojom::NearbyConnectionStep::kAcceptingConnectionFinished;
break;
case ConnectionStatus::kWaitingForConnectionToBeAcceptedByRemoteDevice:
reason =
util::NearbyDisconnectionReason::kTimeoutWaitingForConnectionAccepted;
connection_step = mojom::NearbyConnectionStep::
kWaitingForConnectionToBeAcceptedByRemoteDeviceEnded;
break;
default:
NOTREACHED_IN_MIGRATION()
<< "Unexpected timeout with connection status " << connection_status_;
reason = util::NearbyDisconnectionReason::kConnectionLost;
connection_step = mojom::NearbyConnectionStep::kDisconnectionFinished;
break;
}
NotifyConnectionStateChanged(
connection_step,
mojom::NearbyConnectionStepResult::kTimeoutTransitionState);
Disconnect(reason);
}
void NearbyConnectionBrokerImpl::OnMojoDisconnection() {
PA_LOG(INFO) << __func__;
// If there is a mojo disconnect while requesting a connection, we should
// still try to disconnect from the endpoint in case the endpoint was almost
// about to be connected.
if (connection_status_ == ConnectionStatus::kRequestingConnection) {
need_to_disconnect_endpoint_ = true;
}
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::RegisterPayloadFile(
int64_t payload_id,
mojom::PayloadFilesPtr payload_files,
mojo::PendingRemote<mojom::FilePayloadListener> listener,
RegisterPayloadFileCallback callback) {
nearby_connections_->RegisterPayloadFile(
mojom::kServiceId, payload_id, std::move(payload_files->input_file),
std::move(payload_files->output_file),
base::BindOnce(&NearbyConnectionBrokerImpl::OnPayloadFileRegistered,
weak_ptr_factory_.GetWeakPtr(), payload_id,
std::move(listener), std::move(callback)));
}
void NearbyConnectionBrokerImpl::OnPayloadFileRegistered(
int64_t payload_id,
mojo::PendingRemote<mojom::FilePayloadListener> listener,
RegisterPayloadFileCallback callback,
Status status) {
bool success = status == Status::kSuccess;
if (success) {
mojo::Remote<mojom::FilePayloadListener> listener_remote(
std::move(listener));
// Safe to use Unretained because the Remote and its disconnect handler does
// not out live NearbyConnectionBrokerImpl.
listener_remote.set_disconnect_handler(base::BindOnce(
&NearbyConnectionBrokerImpl::OnFilePayloadListenerDisconnect,
base::Unretained(this), payload_id));
file_payload_listeners_.emplace(payload_id, std::move(listener_remote));
}
std::move(callback).Run(success);
util::RecordRegisterPayloadFilesResult(status);
}
void NearbyConnectionBrokerImpl::OnFilePayloadListenerDisconnect(
int64_t payload_id) {
file_payload_listeners_.erase(payload_id);
}
void NearbyConnectionBrokerImpl::CleanUpPendingFileTransfers() {
for (auto& id_to_listener : file_payload_listeners_) {
id_to_listener.second->OnFileTransferUpdate(mojom::FileTransferUpdate::New(
id_to_listener.first, mojom::FileTransferStatus::kCanceled,
/*total_bytes=*/0,
/*bytes_transferred=*/0));
util::LogFileTransferResult(
util::FileTransferResult::kFileTransferCanceled);
}
file_payload_listeners_.clear();
}
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;
}
// Ignore in the event we are currently disconnecting. Either
// OnConnectionRejected or OnDisconnected will be called eventually.
if (connection_status_ == ConnectionStatus::kDisconnecting) {
return;
}
DCHECK_EQ(ConnectionStatus::kRequestingConnection, connection_status_);
TransitionToStatus(ConnectionStatus::kAcceptingConnection);
NotifyConnectionStateChanged(
mojom::NearbyConnectionStep::kAcceptingConnectionStarted,
mojom::NearbyConnectionStepResult::kSuccess);
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(connection_status_ == ConnectionStatus::kAcceptingConnection ||
connection_status_ ==
ConnectionStatus::kWaitingForConnectionToBeAcceptedByRemoteDevice);
if (connection_status_ == ConnectionStatus::kAcceptingConnection) {
NotifyConnectionStateChanged(
mojom::NearbyConnectionStep::kAcceptingConnectionFinished,
mojom::NearbyConnectionStepResult::kSuccess);
} else if (connection_status_ ==
ConnectionStatus::
kWaitingForConnectionToBeAcceptedByRemoteDevice) {
NotifyConnectionStateChanged(
mojom::NearbyConnectionStep::
kWaitingForConnectionToBeAcceptedByRemoteDeviceEnded,
mojom::NearbyConnectionStepResult::kSuccess);
}
TransitionToStatus(ConnectionStatus::kConnected);
NotifyConnectionStateChanged(mojom::NearbyConnectionStep::kConnected,
mojom::NearbyConnectionStepResult::kSuccess);
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;
}
if (connection_status_ == ConnectionStatus::kDisconnecting) {
// If this callback is invoked while we are disconnecting, we can consider
// the disconnect successful.
need_to_disconnect_endpoint_ = false;
Disconnect(
util::NearbyDisconnectionReason::kDisconnectionRequestedByClient);
return;
}
PA_LOG(WARNING) << "Connection rejected: " << status;
mojom::NearbyConnectionStep connection_step;
switch (connection_status_) {
case ConnectionStatus::kDiscoveringEndpoint:
connection_step = mojom::NearbyConnectionStep::kDiscoveringEndpointEnded;
break;
case ConnectionStatus::kRequestingConnection:
connection_step = mojom::NearbyConnectionStep::kRequestingConnectionEnded;
break;
case ConnectionStatus::kAcceptingConnection:
connection_step =
mojom::NearbyConnectionStep::kAcceptingConnectionFinished;
break;
case ConnectionStatus::kWaitingForConnectionToBeAcceptedByRemoteDevice:
connection_step = mojom::NearbyConnectionStep::
kWaitingForConnectionToBeAcceptedByRemoteDeviceEnded;
break;
default:
NOTREACHED_IN_MIGRATION()
<< "Unexpected connection status when connection rejected"
<< connection_status_;
connection_step = mojom::NearbyConnectionStep::kDiscoveringEndpointEnded;
}
NotifyConnectionStateChanged(
connection_step, mojom::NearbyConnectionStepResult::kConnectionRejected);
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) {
has_upgraded_to_webrtc_ = true;
RecordConnectionMediumMetric(ConnectionMedium::kUpgradedToWebRtc);
NotifyConnectionStateChanged(mojom::NearbyConnectionStep::kUpgradedToWebRtc,
mojom::NearbyConnectionStepResult::kSuccess);
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(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);
} else if (ash::features::IsPhoneHubCameraRollEnabled() &&
payload->content->is_file()) {
if (!file_payload_listeners_.contains(payload->id)) {
PA_LOG(WARNING)
<< "OnPayloadReceived(): Received unregistered file payload with ID "
<< payload->id << ". Disconnecting.";
util::LogFileAction(util::FileAction::kUnexpectedFileReceived);
Disconnect(
util::NearbyDisconnectionReason::kReceivedUnregisteredFilePayload);
} else {
PA_LOG(VERBOSE) << "OnPayloadReceived(): Received file with payload ID "
<< payload->id;
util::LogFileAction(util::FileAction::kRegisteredFileReceived);
}
// We don't need to use the base::File provided by |payload| and it should
// be closed in a task that may block. Otherwise the file will be closed on
// the current thread when |payload| goes out of scope, which would result
// in a DCHECK failure because base::File::Close() is a blocking call.
task_runner_->DeleteSoon(
FROM_HERE, std::make_unique<base::File>(
std::move(payload->content->get_file()->file)));
} else {
PA_LOG(WARNING) << "OnPayloadReceived(): Received unexpected payload type "
<< "(was expecting bytes type). Disconnecting.";
Disconnect(util::NearbyDisconnectionReason::kReceivedUnexpectedPayloadType);
}
}
mojom::FileTransferStatus ConvertFileTransferStatus(PayloadStatus status) {
switch (status) {
case PayloadStatus::kSuccess:
return mojom::FileTransferStatus::kSuccess;
case PayloadStatus::kFailure:
return mojom::FileTransferStatus::kFailure;
case PayloadStatus::kInProgress:
return mojom::FileTransferStatus::kInProgress;
case PayloadStatus::kCanceled:
return mojom::FileTransferStatus::kCanceled;
}
}
void NearbyConnectionBrokerImpl::OnPayloadTransferUpdate(
const std::string& endpoint_id,
::nearby::connections::mojom::PayloadTransferUpdatePtr update) {
if (!ash::features::IsPhoneHubCameraRollEnabled()) {
return;
}
if (remote_endpoint_id_ != endpoint_id) {
PA_LOG(WARNING) << "OnPayloadTransferUpdate(): unexpected endpoint ID; "
<< "expected=" << endpoint_id
<< ", actual=" << remote_endpoint_id_;
return;
}
auto it = file_payload_listeners_.find(update->payload_id);
if (it == file_payload_listeners_.end()) {
return;
}
PA_LOG(VERBOSE)
<< "OnPayloadTransferUpdate(): Received update for file payload "
<< update->payload_id;
it->second->OnFileTransferUpdate(mojom::FileTransferUpdate::New(
update->payload_id, ConvertFileTransferStatus(update->status),
update->total_bytes, update->bytes_transferred));
bool is_transfer_complete = false;
switch (update->status) {
case PayloadStatus::kInProgress:
return;
case PayloadStatus::kSuccess:
is_transfer_complete = true;
util::LogFileTransferResult(
util::FileTransferResult::kFileTransferSuccess);
break;
case PayloadStatus::kFailure:
is_transfer_complete = true;
util::LogFileTransferResult(
util::FileTransferResult::kFileTransferFailure);
break;
case PayloadStatus::kCanceled:
is_transfer_complete = true;
util::LogFileTransferResult(
util::FileTransferResult::kFileTransferCanceled);
break;
}
if (is_transfer_complete) {
file_payload_listeners_.erase(it);
}
}
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 ash