blob: cf5dd3ea929ecdcb99cdc7ad8dced3d1e171872a [file] [log] [blame]
// Copyright 2017 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 "chromeos/components/tether/ble_connection_manager.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "chromeos/components/proximity_auth/logging/logging.h"
#include "chromeos/components/tether/timer_factory.h"
#include "chromeos/services/secure_channel/ble_constants.h"
#include "components/cryptauth/ble/bluetooth_low_energy_weave_client_connection.h"
#include "components/cryptauth/remote_device_ref.h"
#include "device/bluetooth/bluetooth_uuid.h"
namespace chromeos {
namespace tether {
namespace {
const char kTetherFeature[] = "magic_tether";
std::string StateChangeDetailToString(
BleConnectionManager::StateChangeDetail state_change_detail) {
switch (state_change_detail) {
case BleConnectionManager::StateChangeDetail::STATE_CHANGE_DETAIL_NONE:
return "[none]";
case BleConnectionManager::StateChangeDetail::
STATE_CHANGE_DETAIL_COULD_NOT_ATTEMPT_CONNECTION:
return "[could not attempt connection]";
case BleConnectionManager::StateChangeDetail::
STATE_CHANGE_DETAIL_GATT_CONNECTION_WAS_ATTEMPTED:
return "[GATT connection was attempted]";
case BleConnectionManager::StateChangeDetail::
STATE_CHANGE_DETAIL_INTERRUPTED_BY_HIGHER_PRIORITY:
return "[attempt interrupted by higher priority]";
case BleConnectionManager::StateChangeDetail::
STATE_CHANGE_DETAIL_DEVICE_WAS_UNREGISTERED:
return "[device was unregistered]";
default:
NOTREACHED();
return std::string();
}
}
} // namespace
const int64_t BleConnectionManager::kAdvertisingTimeoutMillis = 12000;
const int64_t BleConnectionManager::kFailImmediatelyTimeoutMillis = 0;
BleConnectionManager::ConnectionMetadata::ConnectionMetadata(
const std::string& device_id,
std::unique_ptr<base::OneShotTimer> timer,
base::WeakPtr<BleConnectionManager> manager)
: device_id_(device_id),
connection_attempt_timeout_timer_(std::move(timer)),
manager_(manager),
weak_ptr_factory_(this) {}
BleConnectionManager::ConnectionMetadata::~ConnectionMetadata() = default;
void BleConnectionManager::ConnectionMetadata::RegisterConnectionRequest(
const base::UnguessableToken& request_id,
secure_channel::ConnectionPriority connection_priority) {
DCHECK(!base::ContainsKey(request_id_to_priority_map_, request_id));
request_id_to_priority_map_.insert(
std::make_pair(request_id, connection_priority));
}
void BleConnectionManager::ConnectionMetadata::UnregisterConnectionRequest(
const base::UnguessableToken& request_id) {
request_id_to_priority_map_.erase(request_id);
}
secure_channel::ConnectionPriority
BleConnectionManager::ConnectionMetadata::GetConnectionPriority() {
DCHECK(HasPendingConnectionRequests());
secure_channel::ConnectionPriority highest_priority =
secure_channel::ConnectionPriority::kLow;
for (const auto& map_entry : request_id_to_priority_map_) {
if (map_entry.second > highest_priority)
highest_priority = map_entry.second;
}
return highest_priority;
}
bool BleConnectionManager::ConnectionMetadata::HasPendingConnectionRequests()
const {
return !request_id_to_priority_map_.empty();
}
bool BleConnectionManager::ConnectionMetadata::HasEstablishedConnection()
const {
return secure_channel_.get();
}
cryptauth::SecureChannel::Status
BleConnectionManager::ConnectionMetadata::GetStatus() const {
if (connection_attempt_timeout_timer_->IsRunning()) {
// If the timer is running, a connection attempt is in progress but a
// channel has not been established.
return cryptauth::SecureChannel::Status::CONNECTING;
} else if (!HasEstablishedConnection()) {
// If there is no timer and a channel has not been established, the channel
// is disconnected.
return cryptauth::SecureChannel::Status::DISCONNECTED;
}
// If a channel has been established, return its status.
return secure_channel_->status();
}
void BleConnectionManager::ConnectionMetadata::StartConnectionAttemptTimer(
bool fail_immediately) {
DCHECK(!secure_channel_);
DCHECK(!connection_attempt_timeout_timer_->IsRunning());
int64_t timeout_millis = fail_immediately ? kFailImmediatelyTimeoutMillis
: kAdvertisingTimeoutMillis;
connection_attempt_timeout_timer_->Start(
FROM_HERE, base::TimeDelta::FromMilliseconds(timeout_millis),
base::Bind(&ConnectionMetadata::OnConnectionAttemptTimeout,
weak_ptr_factory_.GetWeakPtr()));
}
void BleConnectionManager::ConnectionMetadata::StopConnectionAttemptTimer() {
DCHECK(!secure_channel_);
connection_attempt_timeout_timer_->Stop();
}
void BleConnectionManager::ConnectionMetadata::OnConnectionAttemptTimeout() {
manager_->OnConnectionAttemptTimeout(device_id_);
}
bool BleConnectionManager::ConnectionMetadata::HasSecureChannel() {
return secure_channel_ != nullptr;
}
void BleConnectionManager::ConnectionMetadata::SetSecureChannel(
std::unique_ptr<cryptauth::SecureChannel> secure_channel) {
DCHECK(!secure_channel_);
// The connection has succeeded, so cancel the timeout.
connection_attempt_timeout_timer_->Stop();
secure_channel_ = std::move(secure_channel);
secure_channel_->AddObserver(this);
secure_channel_->Initialize();
}
int BleConnectionManager::ConnectionMetadata::SendMessage(
const std::string& payload) {
DCHECK(GetStatus() == cryptauth::SecureChannel::Status::AUTHENTICATED);
return secure_channel_->SendMessage(std::string(kTetherFeature), payload);
}
void BleConnectionManager::ConnectionMetadata::Disconnect() {
DCHECK(HasSecureChannel());
secure_channel_->Disconnect();
}
void BleConnectionManager::ConnectionMetadata::OnSecureChannelStatusChanged(
cryptauth::SecureChannel* secure_channel,
const cryptauth::SecureChannel::Status& old_status,
const cryptauth::SecureChannel::Status& new_status) {
DCHECK(secure_channel_.get() == secure_channel);
if (new_status == cryptauth::SecureChannel::Status::CONNECTING) {
// BleConnectionManager already broadcasts "disconnected => connecting"
// status updates when a connection attempt begins, so there is no need to
// handle this case.
return;
}
// Make a copy of the two statuses. If |secure_channel_.reset()| is called
// below, the SecureChannel instance will be destroyed and |old_status| and
// |new_status| may refer to memory which has been deleted.
const cryptauth::SecureChannel::Status old_status_copy = old_status;
const cryptauth::SecureChannel::Status new_status_copy = new_status;
StateChangeDetail state_change_detail =
StateChangeDetail::STATE_CHANGE_DETAIL_NONE;
if (new_status == cryptauth::SecureChannel::Status::DISCONNECTED) {
secure_channel_->RemoveObserver(this);
secure_channel_.reset();
state_change_detail =
StateChangeDetail::STATE_CHANGE_DETAIL_GATT_CONNECTION_WAS_ATTEMPTED;
}
manager_->OnSecureChannelStatusChanged(device_id_, old_status_copy,
new_status_copy, state_change_detail);
}
void BleConnectionManager::ConnectionMetadata::OnMessageReceived(
cryptauth::SecureChannel* secure_channel,
const std::string& feature,
const std::string& payload) {
DCHECK(secure_channel_.get() == secure_channel);
if (feature != std::string(kTetherFeature)) {
// If the message received was not a tether feature, ignore it.
return;
}
manager_->NotifyMessageReceived(device_id_, payload);
}
void BleConnectionManager::ConnectionMetadata::OnMessageSent(
cryptauth::SecureChannel* secure_channel,
int sequence_number) {
DCHECK(secure_channel_.get() == secure_channel);
PA_LOG(INFO) << "Message sent successfully to device with ID \""
<< cryptauth::RemoteDeviceRef::TruncateDeviceIdForLogs(
device_id_)
<< "\"; message sequence number: " << sequence_number;
manager_->NotifyMessageSent(sequence_number);
}
BleConnectionManager::BleConnectionManager(
scoped_refptr<device::BluetoothAdapter> adapter,
BleAdvertisementDeviceQueue* ble_advertisement_device_queue,
BleAdvertiser* ble_advertiser,
BleScanner* ble_scanner)
: adapter_(adapter),
ble_advertisement_device_queue_(ble_advertisement_device_queue),
ble_advertiser_(ble_advertiser),
ble_scanner_(ble_scanner),
timer_factory_(std::make_unique<TimerFactory>()),
has_registered_observer_(false),
weak_ptr_factory_(this) {}
BleConnectionManager::~BleConnectionManager() {
if (has_registered_observer_) {
ble_scanner_->RemoveObserver(this);
}
}
void BleConnectionManager::RegisterRemoteDevice(
const std::string& device_id,
const base::UnguessableToken& request_id,
secure_channel::ConnectionPriority connection_priority) {
if (!has_registered_observer_) {
ble_scanner_->AddObserver(this);
}
has_registered_observer_ = true;
PA_LOG(INFO) << "Register - Device ID: \""
<< cryptauth::RemoteDeviceRef::TruncateDeviceIdForLogs(device_id)
<< "\", Request ID: " << request_id
<< ", Priority: " << connection_priority;
ConnectionMetadata* connection_metadata = GetConnectionMetadata(device_id);
if (!connection_metadata)
connection_metadata = AddMetadataForDevice(device_id);
connection_metadata->RegisterConnectionRequest(request_id,
connection_priority);
UpdateConnectionAttempts();
}
void BleConnectionManager::UnregisterRemoteDevice(
const std::string& device_id,
const base::UnguessableToken& request_id) {
ConnectionMetadata* connection_metadata = GetConnectionMetadata(device_id);
if (!connection_metadata) {
PA_LOG(WARNING) << "Tried to unregister device, but was not registered - "
<< "Device ID: \""
<< cryptauth::RemoteDeviceRef::TruncateDeviceIdForLogs(
device_id)
<< "\", Request ID: " << request_id;
return;
}
PA_LOG(INFO) << "Unregister - Device ID: \""
<< cryptauth::RemoteDeviceRef::TruncateDeviceIdForLogs(device_id)
<< "\", Request ID: " << request_id;
connection_metadata->UnregisterConnectionRequest(request_id);
if (!connection_metadata->HasPendingConnectionRequests()) {
if (connection_metadata->HasEstablishedConnection()) {
connection_metadata->Disconnect();
} else {
// |device_id| references memory that will be deleted below; make a copy.
const std::string device_id_copy = device_id;
cryptauth::SecureChannel::Status status_before_erase =
connection_metadata->GetStatus();
device_id_to_metadata_map_.erase(device_id_copy);
if (status_before_erase == cryptauth::SecureChannel::Status::CONNECTING) {
StopConnectionAttemptAndMoveToEndOfQueue(device_id_copy);
NotifySecureChannelStatusChanged(
device_id_copy, cryptauth::SecureChannel::Status::CONNECTING,
cryptauth::SecureChannel::Status::DISCONNECTED,
StateChangeDetail::STATE_CHANGE_DETAIL_DEVICE_WAS_UNREGISTERED);
}
}
}
UpdateConnectionAttempts();
}
int BleConnectionManager::SendMessage(const std::string& device_id,
const std::string& message) {
ConnectionMetadata* connection_metadata = GetConnectionMetadata(device_id);
if (!connection_metadata ||
connection_metadata->GetStatus() !=
cryptauth::SecureChannel::Status::AUTHENTICATED) {
PA_LOG(ERROR) << "SendMessage(): Error - no authenticated channel. "
<< "Device ID: \""
<< cryptauth::RemoteDeviceRef::TruncateDeviceIdForLogs(
device_id)
<< "\", Message: \"" << message << "\"";
return -1;
}
PA_LOG(INFO) << "SendMessage(): Device ID: \""
<< cryptauth::RemoteDeviceRef::TruncateDeviceIdForLogs(device_id)
<< "\", Message: \"" << message << "\"";
return connection_metadata->SendMessage(message);
}
bool BleConnectionManager::GetStatusForDevice(
const std::string& device_id,
cryptauth::SecureChannel::Status* status) const {
ConnectionMetadata* connection_metadata = GetConnectionMetadata(device_id);
if (!connection_metadata)
return false;
*status = connection_metadata->GetStatus();
return true;
}
void BleConnectionManager::AddObserver(Observer* observer) {
observer_list_.AddObserver(observer);
}
void BleConnectionManager::RemoveObserver(Observer* observer) {
observer_list_.RemoveObserver(observer);
}
void BleConnectionManager::AddMetricsObserver(MetricsObserver* observer) {
metrics_observer_list_.AddObserver(observer);
}
void BleConnectionManager::RemoveMetricsObserver(MetricsObserver* observer) {
metrics_observer_list_.RemoveObserver(observer);
}
void BleConnectionManager::OnReceivedAdvertisementFromDevice(
cryptauth::RemoteDeviceRef remote_device,
device::BluetoothDevice* bluetooth_device,
bool is_background_advertisement) {
const std::string device_id = remote_device.GetDeviceId();
ConnectionMetadata* connection_metadata = GetConnectionMetadata(device_id);
if (!connection_metadata) {
// If an advertisement is received from a device that is not registered,
// ignore it.
PA_LOG(WARNING) << "Received an advertisement from a device which is not "
<< "registered. Bluetooth address: "
<< bluetooth_device->GetAddress() << ", Remote Device "
<< "ID: \"" << remote_device.GetTruncatedDeviceIdForLogs()
<< "\".";
return;
}
if (connection_metadata->HasSecureChannel()) {
PA_LOG(WARNING) << "Received another advertisement from a registered "
<< "device which is already being actively communicated "
<< "with. Bluetooth address: "
<< bluetooth_device->GetAddress() << ", Remote Device "
<< "ID: \"" << remote_device.GetTruncatedDeviceIdForLogs()
<< "\".";
return;
}
PA_LOG(INFO) << "Received advertisement - Device ID: \""
<< remote_device.GetTruncatedDeviceIdForLogs()
<< "\". Starting authentication handshake.";
device_id_to_is_background_advertisement_map_[device_id] =
is_background_advertisement;
NotifyAdvertisementReceived(device_id, is_background_advertisement);
// Stop trying to connect to that device, since it has been found.
StopConnectionAttemptAndMoveToEndOfQueue(device_id);
// Create a connection to that device.
std::unique_ptr<cryptauth::Connection> connection = cryptauth::weave::
BluetoothLowEnergyWeaveClientConnection::Factory::NewInstance(
remote_device, adapter_,
device::BluetoothUUID(secure_channel::kGattServerUuid),
bluetooth_device, false /* should_set_low_connection_latency */);
std::unique_ptr<cryptauth::SecureChannel> secure_channel =
cryptauth::SecureChannel::Factory::NewInstance(std::move(connection));
connection_metadata->SetSecureChannel(std::move(secure_channel));
UpdateConnectionAttempts();
}
BleConnectionManager::ConnectionMetadata*
BleConnectionManager::GetConnectionMetadata(
const std::string& device_id) const {
const auto map_iter = device_id_to_metadata_map_.find(device_id);
if (map_iter == device_id_to_metadata_map_.end())
return nullptr;
return map_iter->second.get();
}
BleConnectionManager::ConnectionMetadata*
BleConnectionManager::AddMetadataForDevice(const std::string& device_id) {
ConnectionMetadata* existing_data = GetConnectionMetadata(device_id);
if (existing_data)
return existing_data;
// Create the metadata.
std::unique_ptr<ConnectionMetadata> metadata = base::WrapUnique(
new ConnectionMetadata(device_id, timer_factory_->CreateOneShotTimer(),
weak_ptr_factory_.GetWeakPtr()));
ConnectionMetadata* metadata_raw_ptr = metadata.get();
// Add it to the map.
device_id_to_metadata_map_.emplace(
std::pair<std::string, std::unique_ptr<ConnectionMetadata>>(
device_id, std::move(metadata)));
return metadata_raw_ptr;
}
void BleConnectionManager::UpdateConnectionAttempts() {
UpdateAdvertisementQueue();
std::vector<std::string> should_advertise_to =
ble_advertisement_device_queue_->GetDeviceIdsToWhichToAdvertise();
DCHECK(should_advertise_to.size() <=
secure_channel::kMaxConcurrentAdvertisements);
// Generate a list of devices which are advertising but are not present in
// |should_advertise_to|.
std::vector<std::string> device_ids_to_stop;
for (const auto& map_entry : device_id_to_metadata_map_) {
if (map_entry.second->GetStatus() ==
cryptauth::SecureChannel::Status::CONNECTING &&
!map_entry.second->HasEstablishedConnection() &&
!base::ContainsValue(should_advertise_to, map_entry.first)) {
device_ids_to_stop.push_back(map_entry.first);
}
}
// For each device that should not be advertised to, end the connection
// attempt. Note that this is done outside of the map iteration above because
// it is possible that EndSuccessfulAttempt() will cause that map to be
// modified during iteration.
for (const auto& device_id_to_stop : device_ids_to_stop) {
PA_LOG(INFO) << "Connection attempt for device ID \""
<< cryptauth::RemoteDeviceRef::TruncateDeviceIdForLogs(
device_id_to_stop)
<< "\" interrupted by higher-priority connection.";
EndUnsuccessfulAttempt(
device_id_to_stop,
StateChangeDetail::STATE_CHANGE_DETAIL_INTERRUPTED_BY_HIGHER_PRIORITY);
}
for (const auto& device_id : should_advertise_to) {
ConnectionMetadata* associated_data = GetConnectionMetadata(device_id);
if (associated_data->GetStatus() !=
cryptauth::SecureChannel::Status::CONNECTING) {
// If there is no active attempt to connect to a device at the front of
// the queue, start a connection attempt.
StartConnectionAttempt(device_id);
}
}
}
void BleConnectionManager::UpdateAdvertisementQueue() {
std::vector<BleAdvertisementDeviceQueue::PrioritizedDeviceId> prioritized_ids;
for (const auto& map_entry : device_id_to_metadata_map_) {
if (map_entry.second->HasEstablishedConnection()) {
// If there is already an active connection to the device, there is no
// need to advertise to the device to bootstrap a connection.
continue;
}
prioritized_ids.emplace_back(map_entry.first,
map_entry.second->GetConnectionPriority());
}
ble_advertisement_device_queue_->SetPrioritizedDeviceIds(prioritized_ids);
}
void BleConnectionManager::StartConnectionAttempt(
const std::string& device_id) {
ConnectionMetadata* connection_metadata = GetConnectionMetadata(device_id);
DCHECK(connection_metadata);
PA_LOG(INFO) << "Attempting connection - Device ID: \""
<< cryptauth::RemoteDeviceRef::TruncateDeviceIdForLogs(device_id)
<< "\"";
bool success = ble_scanner_->RegisterScanFilterForDevice(device_id) &&
ble_advertiser_->StartAdvertisingToDevice(device_id);
// Start a timer; if a connection is unable to be created before the timer
// fires, a timeout occurs. Note that if this class is unable to start both
// the scanner and advertiser successfully (i.e., |success| is |false|), a
// the connection fails immediately insetad of waiting for a timeout, which
// has the effect of quickly sending out "disconnected => connecting =>
// disconnecting" status updates. The timer is used here instead of a special
// case in order to route all connection failures through the same code path.
connection_metadata->StartConnectionAttemptTimer(
!success /* fail_immediately */);
// Send a "disconnected => connecting" update to alert clients that a
// connection attempt for |device_id| is underway.
NotifySecureChannelStatusChanged(
device_id, cryptauth::SecureChannel::Status::DISCONNECTED,
cryptauth::SecureChannel::Status::CONNECTING,
StateChangeDetail::STATE_CHANGE_DETAIL_NONE);
}
void BleConnectionManager::EndUnsuccessfulAttempt(
const std::string& device_id,
StateChangeDetail state_change_detail) {
GetConnectionMetadata(device_id)->StopConnectionAttemptTimer();
StopConnectionAttemptAndMoveToEndOfQueue(device_id);
// Send a "connecting => disconnected" update to alert clients that a
// connection attempt for |device_id| has failed.
NotifySecureChannelStatusChanged(
device_id, cryptauth::SecureChannel::Status::CONNECTING,
cryptauth::SecureChannel::Status::DISCONNECTED, state_change_detail);
}
void BleConnectionManager::StopConnectionAttemptAndMoveToEndOfQueue(
const std::string& device_id) {
ble_scanner_->UnregisterScanFilterForDevice(device_id);
ble_advertiser_->StopAdvertisingToDevice(device_id);
ble_advertisement_device_queue_->MoveDeviceToEnd(device_id);
}
void BleConnectionManager::OnConnectionAttemptTimeout(
const std::string& device_id) {
PA_LOG(INFO) << "Connection attempt timeout - Device ID \""
<< cryptauth::RemoteDeviceRef::TruncateDeviceIdForLogs(device_id)
<< "\".";
EndUnsuccessfulAttempt(
device_id,
StateChangeDetail::STATE_CHANGE_DETAIL_COULD_NOT_ATTEMPT_CONNECTION);
UpdateConnectionAttempts();
}
void BleConnectionManager::OnSecureChannelStatusChanged(
const std::string& device_id,
const cryptauth::SecureChannel::Status& old_status,
const cryptauth::SecureChannel::Status& new_status,
StateChangeDetail state_change_detail) {
ConnectionMetadata* connection_metadata = GetConnectionMetadata(device_id);
DCHECK(connection_metadata);
// Create copies of the references passed to this function. If the map entry
// is erased below, the references will point to deleted memory.
const std::string device_id_copy = device_id;
const cryptauth::SecureChannel::Status old_status_copy = old_status;
const cryptauth::SecureChannel::Status new_status_copy = new_status;
if (!connection_metadata->HasPendingConnectionRequests() &&
new_status == cryptauth::SecureChannel::Status::DISCONNECTED) {
device_id_to_metadata_map_.erase(device_id_copy);
state_change_detail =
StateChangeDetail::STATE_CHANGE_DETAIL_DEVICE_WAS_UNREGISTERED;
}
NotifySecureChannelStatusChanged(device_id_copy, old_status_copy,
new_status_copy, state_change_detail);
UpdateConnectionAttempts();
}
void BleConnectionManager::NotifyAdvertisementReceived(
const std::string& device_id,
bool is_background_advertisement) {
for (auto& observer : metrics_observer_list_)
observer.OnAdvertisementReceived(device_id, is_background_advertisement);
}
void BleConnectionManager::NotifyMessageReceived(std::string device_id,
std::string payload) {
PA_LOG(INFO) << "Message received - Device ID: \""
<< cryptauth::RemoteDeviceRef::TruncateDeviceIdForLogs(device_id)
<< "\", Message: \"" << payload << "\".";
for (auto& observer : observer_list_)
observer.OnMessageReceived(device_id, payload);
}
void BleConnectionManager::NotifySecureChannelStatusChanged(
std::string device_id,
cryptauth::SecureChannel::Status old_status,
cryptauth::SecureChannel::Status new_status,
StateChangeDetail state_change_detail) {
PA_LOG(INFO) << "Status change - Device ID: \""
<< cryptauth::RemoteDeviceRef::TruncateDeviceIdForLogs(device_id)
<< "\": " << cryptauth::SecureChannel::StatusToString(old_status)
<< " => " << cryptauth::SecureChannel::StatusToString(new_status)
<< ", State change detail: "
<< StateChangeDetailToString(state_change_detail);
for (auto& observer : metrics_observer_list_) {
if (old_status == cryptauth::SecureChannel::Status::DISCONNECTED &&
new_status == cryptauth::SecureChannel::Status::CONNECTING) {
observer.OnConnectionAttemptStarted(device_id);
} else if (new_status == cryptauth::SecureChannel::Status::CONNECTED) {
observer.OnConnection(
device_id, device_id_to_is_background_advertisement_map_[device_id]);
} else if (new_status == cryptauth::SecureChannel::Status::AUTHENTICATED) {
observer.OnSecureChannelCreated(
device_id, device_id_to_is_background_advertisement_map_[device_id]);
} else if (new_status == cryptauth::SecureChannel::Status::DISCONNECTED) {
observer.OnDeviceDisconnected(
device_id, state_change_detail,
device_id_to_is_background_advertisement_map_[device_id]);
}
}
for (auto& observer : observer_list_) {
observer.OnSecureChannelStatusChanged(device_id, old_status, new_status,
state_change_detail);
}
}
void BleConnectionManager::NotifyMessageSent(int sequence_number) {
for (auto& observer : observer_list_)
observer.OnMessageSent(sequence_number);
}
void BleConnectionManager::SetTestTimerFactoryForTesting(
std::unique_ptr<TimerFactory> test_timer_factory) {
timer_factory_ = std::move(test_timer_factory);
}
} // namespace tether
} // namespace chromeos