chromium/ash/quick_pair/message_stream/message_stream_lookup_impl.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/quick_pair/message_stream/message_stream_lookup_impl.h"

#include "ash/quick_pair/common/constants.h"
#include "ash/quick_pair/common/fast_pair/fast_pair_metrics.h"
#include "base/containers/contains.h"
#include "components/cross_device/logging/logging.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/bluetooth_device.h"
#include "device/bluetooth/bluetooth_socket.h"

namespace {

const device::BluetoothUUID kMessageStreamUuid(
    "df21fe2c-2515-4fdb-8886-f12c4d67927c");
constexpr int kMaxCreateMessageStreamAttempts{6};

// Attempt retry `n` after cooldown period |message_retry_cooldowns[n-1]|.
// These cooldown periods replicate those that Android's Fast Pair service
// mandates.
const std::vector<base::TimeDelta> kCreateMessageStreamRetryCooldowns{
    base::Seconds(2), base::Seconds(4), base::Seconds(8), base::Seconds(16),
    base::Seconds(32)};

}  // namespace

namespace ash {
namespace quick_pair {

std::string MessageStreamLookupImpl::CreateMessageStreamAttemptTypeToString(
    const CreateMessageStreamAttemptType& type) {
  switch (type) {
    case CreateMessageStreamAttemptType::kDeviceConnectedStateChanged:
      return "[DeviceConnectedStateChanged]";
    case CreateMessageStreamAttemptType::kDeviceAdded:
      return "[DeviceAdded]";
    case CreateMessageStreamAttemptType::kDevicePairedChanged:
      return "[DevicePairedChanged]";
    case CreateMessageStreamAttemptType::kDeviceChanged:
      return "[DeviceChanged]";
  }

  NOTREACHED();
}

MessageStreamLookupImpl::MessageStreamLookupImpl() {
  device::BluetoothAdapterFactory::Get()->GetAdapter(base::BindOnce(
      &MessageStreamLookupImpl::OnGetAdapter, weak_ptr_factory_.GetWeakPtr()));
}

void MessageStreamLookupImpl::OnGetAdapter(
    scoped_refptr<device::BluetoothAdapter> adapter) {
  adapter_ = adapter;
  adapter_observation_.Observe(adapter_.get());
}

void MessageStreamLookupImpl::AddObserver(
    MessageStreamLookup::Observer* observer) {
  observers_.AddObserver(observer);
}

void MessageStreamLookupImpl::RemoveObserver(
    MessageStreamLookup::Observer* observer) {
  observers_.RemoveObserver(observer);
}

MessageStreamLookupImpl::~MessageStreamLookupImpl() = default;

MessageStream* MessageStreamLookupImpl::GetMessageStream(
    const std::string& device_address) {
  auto it = message_streams_.find(device_address);
  // If we don't have a MessageStream for the device at |device_address|, return
  // a nullptr.
  if (it == message_streams_.end())
    return nullptr;

  // Return the pointer underneath the unique_ptr to the MessageStream we are
  // owning for the device at |device_address|.
  return it->second.get();
}

void MessageStreamLookupImpl::DevicePairedChanged(
    device::BluetoothAdapter* adapter,
    device::BluetoothDevice* device,
    bool new_paired_status) {
  // This event is triggered for all paired devices when BT is toggled on, so it
  // is important to make sure the device is actively connected or a connection
  // attempt will be issued for the Message Stream service UUID which prevents
  // audio profiles from connecting.
  if (!device->IsConnected()) {
    return;
  }

  // Check to see if the device supports Message Streams.
  if (!device || !base::Contains(device->GetUUIDs(), kMessageStreamUuid)) {
    return;
  }

  // Remove and delete the memory stream for the device, if it exists.
  if (!new_paired_status) {
    AttemptRemoveMessageStream(device->GetAddress());
    return;
  }

  CD_LOG(VERBOSE, Feature::FP)
      << __func__ << ": Attempting to create MessageStream for device = ["
      << device->GetAddress() << "] " << device->GetNameForDisplay();
  AttemptCreateMessageStream(
      device->GetAddress(),
      CreateMessageStreamAttemptType::kDevicePairedChanged);
}

void MessageStreamLookupImpl::DeviceConnectedStateChanged(
    device::BluetoothAdapter* adapter,
    device::BluetoothDevice* device,
    bool is_now_connected) {
  // Check to see if the device supports Message Streams.
  if (!device || !device->IsPaired() ||
      !base::Contains(device->GetUUIDs(), kMessageStreamUuid)) {
    return;
  }

  // Remove and delete the memory stream for the device, if it exists.
  if (!is_now_connected) {
    AttemptRemoveMessageStream(device->GetAddress());
    return;
  }

  CD_LOG(VERBOSE, Feature::FP)
      << __func__ << ": Attempting to create MessageStream for device = ["
      << device->GetAddress() << "] " << device->GetNameForDisplay();
  AttemptCreateMessageStream(
      device->GetAddress(),
      CreateMessageStreamAttemptType::kDeviceConnectedStateChanged);
}

void MessageStreamLookupImpl::DeviceChanged(device::BluetoothAdapter* adapter,
                                            device::BluetoothDevice* device) {
  // Check to see if the device is connected and supports MessageStreams. We
  // need to check if the device is both connected and paired to the adapter
  // because it is possible for a device to be connected to the adapter but not
  // paired (example: a request for the adapter's SDP records).
  if (!device || !(device->IsConnected() && device->IsPaired()) ||
      !base::Contains(device->GetUUIDs(), kMessageStreamUuid)) {
    return;
  }

  CD_LOG(VERBOSE, Feature::FP)
      << __func__
      << ": found connected device. Attempting to create "
         "MessageStream for device = ["
      << device->GetAddress() << "] " << device->GetNameForDisplay();
  AttemptCreateMessageStream(device->GetAddress(),
                             CreateMessageStreamAttemptType::kDeviceChanged);
}

void MessageStreamLookupImpl::DeviceAdded(device::BluetoothAdapter* adapter,
                                          device::BluetoothDevice* device) {
  // Check to see if the device is connected and supports MessageStreams. We
  // need to check if the device is both connected and paired to the adapter
  // because it is possible for a device to be connected to the adapter but not
  // paired (example: a request for the adapter's SDP records).
  if (!device || !(device->IsConnected() && device->IsPaired()) ||
      !base::Contains(device->GetUUIDs(), kMessageStreamUuid)) {
    return;
  }

  CD_LOG(VERBOSE, Feature::FP)
      << __func__
      << ": found connected device. Attempting to create "
         "MessageStream for device = ["
      << device->GetAddress() << "] " << device->GetNameForDisplay();
  AttemptCreateMessageStream(device->GetAddress(),
                             CreateMessageStreamAttemptType::kDeviceAdded);
}

void MessageStreamLookupImpl::DeviceRemoved(device::BluetoothAdapter* adapter,
                                            device::BluetoothDevice* device) {
  if (!device)
    return;

  // Remove message stream if the device removed from the adapter has a
  // message stream and disconnect from socket if applicable. It isn't expected
  // to already have a MessageStream associated with it.
  AttemptEraseMessageStream(device->GetAddress());
}

void MessageStreamLookupImpl::AttemptRemoveMessageStream(
    const std::string& device_address) {
  CD_LOG(VERBOSE, Feature::FP)
      << __func__ << ": device address = " << device_address;
  AttemptEraseMessageStream(device_address);
}

void MessageStreamLookupImpl::AttemptEraseMessageStream(
    const std::string& device_address) {
  CD_LOG(VERBOSE, Feature::FP)
      << __func__ << ": device address = " << device_address;
  // Remove map entry if it exists. It may not exist if it was failed to be
  // created due to a |ConnectToService| error.
  if (!base::Contains(message_streams_, device_address))
    return;

  // If the MessageStream still exists, we can attempt to gracefully disconnect
  // the socket before erasing (and therefore destructing) the MessageStream
  // instance.
  message_streams_[device_address]->Disconnect(
      base::BindOnce(&MessageStreamLookupImpl::OnSocketDisconnected,
                     weak_ptr_factory_.GetWeakPtr(), device_address));
}

void MessageStreamLookupImpl::OnSocketDisconnected(
    const std::string& device_address) {
  message_streams_.erase(device_address);
}

void MessageStreamLookupImpl::AttemptCreateMessageStream(
    const std::string& device_address,
    const CreateMessageStreamAttemptType& type) {
  device::BluetoothDevice* device = adapter_->GetDevice(device_address);
  if (!device) {
    CD_LOG(INFO, Feature::FP)
        << __func__ << ": lost device for Message Stream creation";
    AttemptRemoveMessageStream(device_address);
    return;
  }

  CD_LOG(VERBOSE, Feature::FP)
      << __func__ << ": device address = " << device_address
      << " type = " << CreateMessageStreamAttemptTypeToString(type);

  // Only open MessageStreams for new devices that don't already have a
  // MessageStream stored in the map. We can sometimes reach this point if
  // multiple BluetoothAdapter events fire for a device connected event, but
  // we need all of these BluetoothAdapter observation events to handle
  // different connection scenarios, and have coverage for different devices.
  if (base::Contains(message_streams_, device_address)) {
    CD_LOG(INFO, Feature::FP)
        << __func__ << ": Message Stream exists already for device";
    return;
  }

  if (base::Contains(pending_connect_requests_, device_address)) {
    CD_LOG(INFO, Feature::FP)
        << __func__ << ": Ignoring due to matching pending request";
    return;
  }

  pending_connect_requests_.insert(device_address);

  device->ConnectToService(
      /*uuid=*/kMessageStreamUuid, /*callback=*/
      base::BindOnce(&MessageStreamLookupImpl::OnConnected,
                     weak_ptr_factory_.GetWeakPtr(), device_address,
                     base::TimeTicks::Now(), type),
      /*error_callback=*/
      base::BindOnce(&MessageStreamLookupImpl::OnConnectError,
                     weak_ptr_factory_.GetWeakPtr(), device_address, type));
}

void MessageStreamLookupImpl::OnConnected(
    std::string device_address,
    base::TimeTicks connect_to_service_start_time,
    const CreateMessageStreamAttemptType& type,
    scoped_refptr<device::BluetoothSocket> socket) {
  if (create_message_stream_retry_timers_.contains(device_address)) {
    base::OneShotTimer* curr_create_message_stream_retry_timer =
        create_message_stream_retry_timers_[device_address].get();

    // This if branch should be unnecessary in theory, but it is included to
    // address the edge case that a success occurs after a failure.
    if (curr_create_message_stream_retry_timer->IsRunning())
      curr_create_message_stream_retry_timer->Stop();

    size_t timer_erased_ct =
        create_message_stream_retry_timers_.erase(device_address);
    DCHECK(timer_erased_ct == 1);
    size_t retry_ct_erased_ct =
        create_message_stream_attempts_.erase(device_address);
    DCHECK(retry_ct_erased_ct == 1);
  }

  // It is expected that at the point of a successful RFCOMM connection, the
  // device is known to the adapter.
  device::BluetoothDevice* bt_device = adapter_->GetDevice(device_address);
  DCHECK(bt_device);
  CD_LOG(VERBOSE, Feature::FP)
      << __func__ << ": device = " << device_address
      << " device name = " << bt_device->GetNameForDisplay()
      << " Type = " << CreateMessageStreamAttemptTypeToString(type);

  RecordMessageStreamConnectToServiceResult(/*success=*/true);
  RecordMessageStreamConnectToServiceTime(base::TimeTicks::Now() -
                                          connect_to_service_start_time);

  std::unique_ptr<MessageStream> message_stream =
      std::make_unique<MessageStream>(device_address, std::move(socket));

  for (auto& observer : observers_)
    observer.OnMessageStreamConnected(device_address, message_stream.get());

  message_streams_[device_address] = std::move(message_stream);
  pending_connect_requests_.erase(device_address);
}

void MessageStreamLookupImpl::OnConnectError(
    std::string device_address,
    const CreateMessageStreamAttemptType& type,
    const std::string& error_message) {
  // Because we need to attempt to create MessageStreams at many different
  // iterations due to the variability of Bluetooth APIs, we can expect to
  // see errors here frequently, along with errors followed by a success.
  CD_LOG(INFO, Feature::FP)
      << __func__ << ": Error: [ " << error_message
      << "]. Type: " << CreateMessageStreamAttemptTypeToString(type) << ".";
  RecordMessageStreamConnectToServiceResult(/*success=*/false);
  RecordMessageStreamConnectToServiceError(error_message);
  pending_connect_requests_.erase(device_address);

  // A timer is started to retry AttemptCreateMessageStream if
  // the maximum number of attempts (6) to create the MessageStream has not been
  // reached. If this is the first retry, new entries in
  // |create_message_stream_attempts_| and
  // |create_message_stream_retry_timers_| are created.
  create_message_stream_attempts_.try_emplace(device_address, 1);

  int& create_message_stream_attempt_num =
      create_message_stream_attempts_[device_address];
  if (create_message_stream_attempt_num == kMaxCreateMessageStreamAttempts) {
    CD_LOG(INFO, Feature::FP)
        << __func__
        << ": 6 attempts to create a message stream have failed. "
           "There are no more retries.";
    return;
  }

  device::BluetoothDevice* device = adapter_->GetDevice(device_address);
  if (device) {
    create_message_stream_retry_timers_.try_emplace(
        device_address, std::make_unique<base::OneShotTimer>());

    base::OneShotTimer* curr_create_message_stream_retry_timer =
        create_message_stream_retry_timers_[device_address].get();
    curr_create_message_stream_retry_timer->Start(
        FROM_HERE,
        kCreateMessageStreamRetryCooldowns[create_message_stream_attempt_num++ -
                                           1],
        base::BindOnce(&MessageStreamLookupImpl::AttemptCreateMessageStream,
                       weak_ptr_factory_.GetWeakPtr(), device_address, type));
  } else {
    CD_LOG(INFO, Feature::FP)
        << __func__ << ": attempting to retry message stream creation with "
        << " a device no longer found by the adapter."
        << " device address: " << device_address;
    size_t retry_ct_erased_ct =
        create_message_stream_attempts_.erase(device_address);
    DCHECK(retry_ct_erased_ct == 1);
    create_message_stream_retry_timers_.erase(device_address);
  }
}

}  // namespace quick_pair
}  // namespace ash