chromium/chromeos/ash/services/bluetooth_config/bluetooth_device_status_notifier_impl_unittest.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 "chromeos/ash/services/bluetooth_config/bluetooth_device_status_notifier_impl.h"

#include <memory>
#include <vector>

#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/task_environment.h"
#include "chromeos/ash/services/bluetooth_config/fake_adapter_state_controller.h"
#include "chromeos/ash/services/bluetooth_config/fake_bluetooth_device_status_observer.h"
#include "chromeos/ash/services/bluetooth_config/fake_device_cache.h"
#include "chromeos/ash/services/nearby/public/cpp/nearby_client_uuids.h"
#include "chromeos/dbus/power/fake_power_manager_client.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/bluetooth/test/mock_bluetooth_device.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash::bluetooth_config {

namespace {

const uint32_t kTestBluetoothClass = 1337u;

// Used for devices which are not connected by a nearby share connection.
const char kNonNearbyUuid[] = "00001108-0000-1000-8000-00805f9b34fb";

mojom::PairedBluetoothDevicePropertiesPtr GenerateStubPairedDeviceProperties(
    const std::string& id,
    mojom::DeviceConnectionState connection_state =
        mojom::DeviceConnectionState::kConnected) {
  auto device_properties = mojom::BluetoothDeviceProperties::New();

  // Concate "-Identifier" so device id matches
  // MockBluetoothDevice->GetIdentifier() return value.
  device_properties->id = base::StrCat({id, "-Identifier"});
  device_properties->address = "test_device_address";
  device_properties->public_name = u"name";
  device_properties->device_type = mojom::DeviceType::kUnknown;
  device_properties->audio_capability =
      mojom::AudioOutputCapability::kNotCapableOfAudioOutput;
  device_properties->connection_state = connection_state;

  mojom::PairedBluetoothDevicePropertiesPtr paired_properties =
      mojom::PairedBluetoothDeviceProperties::New();
  paired_properties->device_properties = std::move(device_properties);
  return paired_properties;
}

}  // namespace

class BluetoothDeviceStatusNotifierImplTest : public testing::Test {
 protected:
  BluetoothDeviceStatusNotifierImplTest()
      : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
  BluetoothDeviceStatusNotifierImplTest(
      const BluetoothDeviceStatusNotifierImplTest&) = delete;
  BluetoothDeviceStatusNotifierImplTest& operator=(
      const BluetoothDeviceStatusNotifierImplTest&) = delete;
  ~BluetoothDeviceStatusNotifierImplTest() override = default;

  // testing::Test:
  void SetUp() override {
    chromeos::PowerManagerClient::InitializeFake();

    mock_adapter_ =
        base::MakeRefCounted<testing::NiceMock<device::MockBluetoothAdapter>>();
    ON_CALL(*mock_adapter_, GetDevices())
        .WillByDefault(testing::Invoke(
            this, &BluetoothDeviceStatusNotifierImplTest::GenerateDevices));

    device_status_notifier_ =
        std::make_unique<BluetoothDeviceStatusNotifierImpl>(
            mock_adapter_, &fake_device_cache_,
            chromeos::FakePowerManagerClient::Get());
  }

  void TearDown() override {
    // Destroy |device_status_notifier_| before the fake power manager client in
    // order to remove observers correctly.
    device_status_notifier_.reset();
    chromeos::PowerManagerClient::Shutdown();
  }

  // Pass in a list of ids because MockBluetoothDevice appends "-Identifier"
  // to any value passed in as identifier. We want GetIdentifier() value
  // returned from MockBluetoothDevice to be the same as the id in
  // PairedBluetoothDevicePropertiesPtr. The ids passed in here do not include
  // "-Identifier" prefix.
  void SetPairedDevices(
      const std::vector<mojom::PairedBluetoothDevicePropertiesPtr>&
          paired_devices,
      const std::vector<std::string>& ids,
      const bool includes_nearby_uuids = false) {
    EXPECT_EQ(paired_devices.size(), ids.size());

    std::vector<mojom::PairedBluetoothDevicePropertiesPtr> copy;
    mock_devices_.clear();

    base::flat_set<device::BluetoothUUID> uuid_set;

    if (includes_nearby_uuids) {
      uuid_set.insert(nearby::GetNearbyClientUuids().front());
    } else {
      uuid_set.insert({device::BluetoothUUID(kNonNearbyUuid)});
    }

    for (unsigned int i = 0; i < paired_devices.size(); ++i) {
      copy.push_back(paired_devices[i].Clone());

      bool connected = paired_devices[i]->device_properties->connection_state ==
                       mojom::DeviceConnectionState::kConnected;
      auto mock_device =
          std::make_unique<testing::NiceMock<device::MockBluetoothDevice>>(
              mock_adapter_.get(), kTestBluetoothClass,
              base::UTF16ToASCII(
                  paired_devices[i]->device_properties->public_name)
                  .c_str(),
              ids[i], /*paired=*/true, connected);

      ON_CALL(*mock_device, GetUUIDs())
          .WillByDefault(testing::Return(uuid_set));

      mock_devices_.push_back(std::move(mock_device));
    }

    fake_device_cache_.SetPairedDevices(std::move(copy));
    device_status_notifier_->FlushForTesting();
  }

  std::unique_ptr<FakeBluetoothDeviceStatusObserver> Observe() {
    auto observer = std::make_unique<FakeBluetoothDeviceStatusObserver>();
    device_status_notifier_->ObserveDeviceStatusChanges(
        observer->GeneratePendingRemote());
    device_status_notifier_->FlushForTesting();
    return observer;
  }

  int64_t GetSuspendCooldownTimeoutSeconds() {
    return BluetoothDeviceStatusNotifierImpl::kSuspendCooldownTimeout
        .InSeconds();
  }

  void FastForwardBy(int64_t seconds) {
    task_environment_.FastForwardBy(base::Seconds(seconds));
  }

 private:
  std::vector<raw_ptr<const device::BluetoothDevice, VectorExperimental>>
  GenerateDevices() {
    std::vector<raw_ptr<const device::BluetoothDevice, VectorExperimental>>
        devices;
    for (auto& device : mock_devices_)
      devices.push_back(device.get());
    return devices;
  }

  base::test::TaskEnvironment task_environment_;

  std::vector<std::unique_ptr<testing::NiceMock<device::MockBluetoothDevice>>>
      mock_devices_;
  scoped_refptr<testing::NiceMock<device::MockBluetoothAdapter>> mock_adapter_;

  FakeAdapterStateController fake_adapter_state_controller_;
  FakeDeviceCache fake_device_cache_{&fake_adapter_state_controller_};
  std::unique_ptr<BluetoothDeviceStatusNotifierImpl> device_status_notifier_;
};

TEST_F(BluetoothDeviceStatusNotifierImplTest, PairedDevicesChanges) {
  std::unique_ptr<FakeBluetoothDeviceStatusObserver> observer = Observe();

  // Initially, observer would receive no updates.
  ASSERT_EQ(0u, observer->paired_device_properties_list().size());
  EXPECT_TRUE(observer->paired_device_properties_list().empty());
  ASSERT_EQ(0u, observer->disconnected_device_properties_list().size());
  EXPECT_TRUE(observer->disconnected_device_properties_list().empty());

  std::vector<mojom::PairedBluetoothDevicePropertiesPtr> paired_devices;
  paired_devices.push_back(GenerateStubPairedDeviceProperties(
      "id",
      /*connection_state=*/mojom::DeviceConnectionState::kNotConnected));

  // Add a paired but disconnected device and verify that the observer was
  // not notified.
  SetPairedDevices(paired_devices, /*ids=*/{"id"});
  ASSERT_EQ(0u, observer->paired_device_properties_list().size());
  EXPECT_TRUE(observer->paired_device_properties_list().empty());
  ASSERT_EQ(0u, observer->disconnected_device_properties_list().size());

  paired_devices.pop_back();
  paired_devices.push_back(GenerateStubPairedDeviceProperties(
      "id1",
      /*connection_state=*/mojom::DeviceConnectionState::kConnected));

  // Remove the paired, disconnected device and add a paired, connected device
  // and verify that only the paired list observer was notified.
  SetPairedDevices(paired_devices, /*ids=*/{"id1"});
  ASSERT_EQ(1u, observer->paired_device_properties_list().size());
  ASSERT_EQ(
      "id1-Identifier",
      observer->paired_device_properties_list()[0]->device_properties->id);
  ASSERT_EQ(0u, observer->disconnected_device_properties_list().size());

  paired_devices.push_back(GenerateStubPairedDeviceProperties(
      "id2",
      /*connection_state=*/mojom::DeviceConnectionState::kConnected));
  paired_devices.push_back(GenerateStubPairedDeviceProperties(
      "id3",
      /*connection_state=*/mojom::DeviceConnectionState::kConnected));

  // Add two paired devices and verify that the observer was notified.
  SetPairedDevices(paired_devices, /*ids=*/{"id1", "id2", "id3"});
  ASSERT_EQ(3u, observer->paired_device_properties_list().size());
  ASSERT_EQ(
      "id2-Identifier",
      observer->paired_device_properties_list()[1]->device_properties->id);
  ASSERT_EQ(
      "id3-Identifier",
      observer->paired_device_properties_list()[2]->device_properties->id);
  ASSERT_EQ(0u, observer->disconnected_device_properties_list().size());

  // Simulate device being unpaired, the observer should be notified the device
  // was disconnected.
  paired_devices.pop_back();
  SetPairedDevices(paired_devices, /*ids=*/{"id1", "id2"});
  ASSERT_EQ(3u, observer->paired_device_properties_list().size());
  ASSERT_EQ(1u, observer->disconnected_device_properties_list().size());

  // Add the same device again.
  paired_devices.push_back(GenerateStubPairedDeviceProperties(
      "id3",
      /*connection_state=*/mojom::DeviceConnectionState::kConnected));

  // Verify that the observer was notified.
  SetPairedDevices(paired_devices, /*ids=*/{"id1", "id2", "id3"});
  ASSERT_EQ(4u, observer->paired_device_properties_list().size());
  ASSERT_EQ(
      "id3-Identifier",
      observer->paired_device_properties_list()[3]->device_properties->id);
  ASSERT_EQ(1u, observer->disconnected_device_properties_list().size());
}

TEST_F(BluetoothDeviceStatusNotifierImplTest, ConnectedDevicesChanges) {
  std::unique_ptr<FakeBluetoothDeviceStatusObserver> observer = Observe();

  std::vector<mojom::PairedBluetoothDevicePropertiesPtr> devices;
  devices.push_back(GenerateStubPairedDeviceProperties("id1"));

  // Add a connected device
  SetPairedDevices(devices, /*ids=*/{"id1"});

  // Initially, observer would receive no updates.
  ASSERT_EQ(0u, observer->disconnected_device_properties_list().size());
  EXPECT_TRUE(observer->disconnected_device_properties_list().empty());
  ASSERT_EQ(0u, observer->connected_device_properties_list().size());
  EXPECT_TRUE(observer->connected_device_properties_list().empty());

  ASSERT_EQ(1u, observer->paired_device_properties_list().size());

  // Update device from connected to disconnected.
  devices.pop_back();
  devices.push_back(GenerateStubPairedDeviceProperties(
      "id1",
      /*connection_state=*/mojom::DeviceConnectionState::kNotConnected));

  SetPairedDevices(devices, /*ids=*/{"id1"});

  ASSERT_EQ(1u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ("id1-Identifier", observer->disconnected_device_properties_list()[0]
                                  ->device_properties->id);
  ASSERT_EQ(0u, observer->connected_device_properties_list().size());
  ASSERT_EQ(1u, observer->paired_device_properties_list().size());

  // Update device from disconnected to connected.
  devices.pop_back();
  devices.push_back(GenerateStubPairedDeviceProperties("id1"));
  SetPairedDevices(devices, /*ids=*/{"id1"});

  ASSERT_EQ(1u, observer->connected_device_properties_list().size());
  ASSERT_EQ(
      "id1-Identifier",
      observer->connected_device_properties_list()[0]->device_properties->id);
  ASSERT_EQ(1u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ(1u, observer->paired_device_properties_list().size());

  // Update state from connected to connecting, this should have no effect.
  devices.pop_back();
  devices.push_back(GenerateStubPairedDeviceProperties(
      "id1",
      /*connection_state=*/mojom::DeviceConnectionState::kConnecting));
  SetPairedDevices(devices, /*ids=*/{"id1"});

  ASSERT_EQ(1u, observer->connected_device_properties_list().size());
  ASSERT_EQ(
      "id1-Identifier",
      observer->connected_device_properties_list()[0]->device_properties->id);
  ASSERT_EQ(1u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ(1u, observer->paired_device_properties_list().size());

  // Update device from connecting to connected.
  devices.pop_back();
  devices.push_back(GenerateStubPairedDeviceProperties("id1"));
  SetPairedDevices(devices, /*ids=*/{"id1"});

  ASSERT_EQ(2u, observer->connected_device_properties_list().size());
  ASSERT_EQ(
      "id1-Identifier",
      observer->connected_device_properties_list()[0]->device_properties->id);
  ASSERT_EQ(1u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ(1u, observer->paired_device_properties_list().size());
}

TEST_F(BluetoothDeviceStatusNotifierImplTest, DisconnectToStopObserving) {
  std::unique_ptr<FakeBluetoothDeviceStatusObserver> observer = Observe();

  // Initially, observer would receive no updates.
  ASSERT_EQ(0u, observer->paired_device_properties_list().size());
  EXPECT_TRUE(observer->paired_device_properties_list().empty());

  // Disconnect the Mojo pipe; this should stop observing.
  observer->DisconnectMojoPipe();

  // Add a paired device; the observer should not be notified since it
  // is no longer connected.
  std::vector<mojom::PairedBluetoothDevicePropertiesPtr> paired_devices;
  paired_devices.push_back(GenerateStubPairedDeviceProperties("id1"));
  EXPECT_EQ(0u, observer->paired_device_properties_list().size());
}

TEST_F(BluetoothDeviceStatusNotifierImplTest,
       DeviceDisconnectsConnectsDuringSuspend) {
  std::unique_ptr<FakeBluetoothDeviceStatusObserver> observer = Observe();
  const std::string device_id = "id1";

  // Add a connected device
  std::vector<mojom::PairedBluetoothDevicePropertiesPtr> devices;
  devices.push_back(GenerateStubPairedDeviceProperties(device_id));
  SetPairedDevices(devices, /*ids=*/{device_id});

  ASSERT_EQ(0u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ(0u, observer->connected_device_properties_list().size());
  ASSERT_EQ(1u, observer->paired_device_properties_list().size());

  // Simulate device being suspended from lid closing.
  chromeos::FakePowerManagerClient::Get()->SendSuspendImminent(
      power_manager::SuspendImminent::LID_CLOSED);

  // Update device from connected to disconnected. Observers should not be
  // notified.
  devices.pop_back();
  devices.push_back(GenerateStubPairedDeviceProperties(
      device_id,
      /*connection_state=*/mojom::DeviceConnectionState::kNotConnected));
  SetPairedDevices(devices, /*ids=*/{device_id});

  ASSERT_EQ(0u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ(0u, observer->connected_device_properties_list().size());
  ASSERT_EQ(1u, observer->paired_device_properties_list().size());

  // Update device from disconnected to connected. Observers should be notified.
  devices.pop_back();
  devices.push_back(GenerateStubPairedDeviceProperties(
      device_id,
      /*connection_state=*/mojom::DeviceConnectionState::kConnected));
  SetPairedDevices(devices, /*ids=*/{device_id});

  ASSERT_EQ(0u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ(1u, observer->connected_device_properties_list().size());
  ASSERT_EQ(
      base::StrCat({device_id, "-Identifier"}),
      observer->connected_device_properties_list()[0]->device_properties->id);
  ASSERT_EQ(1u, observer->paired_device_properties_list().size());
}

TEST_F(BluetoothDeviceStatusNotifierImplTest,
       DeviceDisconnectsConnectsAfterSuspendDuringCooldown) {
  std::unique_ptr<FakeBluetoothDeviceStatusObserver> observer = Observe();
  const std::string device_id = "id1";

  // Add a connected device
  std::vector<mojom::PairedBluetoothDevicePropertiesPtr> devices;
  devices.push_back(GenerateStubPairedDeviceProperties(device_id));
  SetPairedDevices(devices, /*ids=*/{device_id});

  ASSERT_EQ(0u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ(0u, observer->connected_device_properties_list().size());
  ASSERT_EQ(1u, observer->paired_device_properties_list().size());

  // Simulate Chromebook being suspended from lid closing.
  chromeos::FakePowerManagerClient::Get()->SendSuspendImminent(
      power_manager::SuspendImminent::LID_CLOSED);

  // Simulate Chromebook being awakened.
  chromeos::FakePowerManagerClient::Get()->SendSuspendDone(
      base::Milliseconds(1000));

  // Update device from connected to disconnected. Observers should not be
  // notified.
  devices.pop_back();
  devices.push_back(GenerateStubPairedDeviceProperties(
      device_id,
      /*connection_state=*/mojom::DeviceConnectionState::kNotConnected));
  SetPairedDevices(devices, /*ids=*/{device_id});

  ASSERT_EQ(0u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ(0u, observer->connected_device_properties_list().size());
  ASSERT_EQ(1u, observer->paired_device_properties_list().size());

  // Update device from disconnected to connected. Observers should be notified.
  devices.pop_back();
  devices.push_back(GenerateStubPairedDeviceProperties(
      device_id,
      /*connection_state=*/mojom::DeviceConnectionState::kConnected));
  SetPairedDevices(devices, /*ids=*/{device_id});

  ASSERT_EQ(0u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ(1u, observer->connected_device_properties_list().size());
  ASSERT_EQ(
      base::StrCat({device_id, "-Identifier"}),
      observer->connected_device_properties_list()[0]->device_properties->id);
  ASSERT_EQ(1u, observer->paired_device_properties_list().size());
}

TEST_F(BluetoothDeviceStatusNotifierImplTest,
       DeviceDisconnectsConnectsAfterSuspendCooldown) {
  std::unique_ptr<FakeBluetoothDeviceStatusObserver> observer = Observe();
  const std::string device_id = "id1";

  // Add a connected device
  std::vector<mojom::PairedBluetoothDevicePropertiesPtr> devices;
  devices.push_back(GenerateStubPairedDeviceProperties(device_id));
  SetPairedDevices(devices, /*ids=*/{device_id});

  ASSERT_EQ(0u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ(0u, observer->connected_device_properties_list().size());
  ASSERT_EQ(1u, observer->paired_device_properties_list().size());

  // Simulate Chromebook being suspended from lid closing.
  chromeos::FakePowerManagerClient::Get()->SendSuspendImminent(
      power_manager::SuspendImminent::LID_CLOSED);

  // Simulate Chromebook being awakened.
  chromeos::FakePowerManagerClient::Get()->SendSuspendDone(
      base::Milliseconds(1000));

  // Simulate the suspend cooldown passing.
  FastForwardBy(GetSuspendCooldownTimeoutSeconds());

  // Update device from connected to disconnected. Observers should be notified.
  devices.pop_back();
  devices.push_back(GenerateStubPairedDeviceProperties(
      device_id,
      /*connection_state=*/mojom::DeviceConnectionState::kNotConnected));
  SetPairedDevices(devices, /*ids=*/{device_id});

  ASSERT_EQ(1u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ(base::StrCat({device_id, "-Identifier"}),
            observer->disconnected_device_properties_list()[0]
                ->device_properties->id);
  ASSERT_EQ(0u, observer->connected_device_properties_list().size());
  ASSERT_EQ(1u, observer->paired_device_properties_list().size());

  // Update device from disconnected to connected. Observers should be notified.
  devices.pop_back();
  devices.push_back(GenerateStubPairedDeviceProperties(
      device_id,
      /*connection_state=*/mojom::DeviceConnectionState::kConnected));
  SetPairedDevices(devices, /*ids=*/{device_id});

  ASSERT_EQ(1u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ(1u, observer->connected_device_properties_list().size());
  ASSERT_EQ(
      base::StrCat({device_id, "-Identifier"}),
      observer->connected_device_properties_list()[0]->device_properties->id);
  ASSERT_EQ(1u, observer->paired_device_properties_list().size());
}

TEST_F(BluetoothDeviceStatusNotifierImplTest,
       DeviceDisconnectsConnectsDuringSecondSuspend) {
  std::unique_ptr<FakeBluetoothDeviceStatusObserver> observer = Observe();
  const std::string device_id = "id1";

  // Add a connected device
  std::vector<mojom::PairedBluetoothDevicePropertiesPtr> devices;
  devices.push_back(GenerateStubPairedDeviceProperties(device_id));
  SetPairedDevices(devices, /*ids=*/{device_id});

  ASSERT_EQ(0u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ(0u, observer->connected_device_properties_list().size());
  ASSERT_EQ(1u, observer->paired_device_properties_list().size());

  // Simulate Chromebook being suspended from lid closing.
  chromeos::FakePowerManagerClient::Get()->SendSuspendImminent(
      power_manager::SuspendImminent::LID_CLOSED);

  // Simulate Chromebook being awakened.
  chromeos::FakePowerManagerClient::Get()->SendSuspendDone(
      base::Milliseconds(1000));

  // Simulate |seconds_forward| seconds passing.
  int seconds_forward = 1;
  FastForwardBy(seconds_forward);

  // Simulate Chromebook being suspended from lid closing again.
  chromeos::FakePowerManagerClient::Get()->SendSuspendImminent(
      power_manager::SuspendImminent::LID_CLOSED);

  // Simulate suspend cooldown time passing. This is where the first timer
  // should timeout if it wasn't canceled.
  FastForwardBy(GetSuspendCooldownTimeoutSeconds() - seconds_forward);

  // Update device from connected to disconnected. Observers should not be
  // notified.
  devices.pop_back();
  devices.push_back(GenerateStubPairedDeviceProperties(
      device_id,
      /*connection_state=*/mojom::DeviceConnectionState::kNotConnected));
  SetPairedDevices(devices, /*ids=*/{device_id});

  ASSERT_EQ(0u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ(0u, observer->connected_device_properties_list().size());
  ASSERT_EQ(1u, observer->paired_device_properties_list().size());

  // Update device from disconnected to connected. Observers should be notified.
  devices.pop_back();
  devices.push_back(GenerateStubPairedDeviceProperties(
      device_id,
      /*connection_state=*/mojom::DeviceConnectionState::kConnected));
  SetPairedDevices(devices, /*ids=*/{device_id});

  ASSERT_EQ(0u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ(1u, observer->connected_device_properties_list().size());
  ASSERT_EQ(
      base::StrCat({device_id, "-Identifier"}),
      observer->connected_device_properties_list()[0]->device_properties->id);
  ASSERT_EQ(1u, observer->paired_device_properties_list().size());
}

TEST_F(BluetoothDeviceStatusNotifierImplTest, NearbyDevicePaired) {
  std::unique_ptr<FakeBluetoothDeviceStatusObserver> observer = Observe();

  std::vector<mojom::PairedBluetoothDevicePropertiesPtr> devices;
  devices.push_back(GenerateStubPairedDeviceProperties("id1"));
  SetPairedDevices(devices, /*ids=*/{"id1"}, /*is_nearby_devices=*/true);

  // Initially, observer would receive no updates.
  ASSERT_EQ(0u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ(0u, observer->connected_device_properties_list().size());
  ASSERT_EQ(0u, observer->paired_device_properties_list().size());

  // Update device from connected to disconnected.
  devices.pop_back();
  devices.push_back(GenerateStubPairedDeviceProperties(
      "id1",
      /*connection_state=*/mojom::DeviceConnectionState::kNotConnected));

  SetPairedDevices(devices, /*ids=*/{"id1"}, /*is_nearby_devices=*/true);

  // No notifications are shown because the device is connected using
  // Nearby connections.
  ASSERT_EQ(0u, observer->disconnected_device_properties_list().size());
  ASSERT_EQ(0u, observer->connected_device_properties_list().size());
  ASSERT_EQ(0u, observer->paired_device_properties_list().size());
}

}  // namespace ash::bluetooth_config