[bt][fidl] Prefer BR/EDR for connections to dual-mode peers

fuchsia.bluetooth.host.Host.Connect now prefers the BR/EDR transport if
the peer supports it. This changes the old behavior that limited
BR/EDR connections to just BR/EDR-only peers.

Bug: fxb/1242, fxb/40004
Test: bt-host-unittests - FIDL_HostServerTest

Change-Id: I62aea9afa8925a0072fd2d2d010da601a91cf7c4
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/334837
Commit-Queue: Arman Uguray <armansito@google.com>
Reviewed-by: Lucas Jenkins <lucasjenkins@google.com>
Testability-Review: Lucas Jenkins <lucasjenkins@google.com>
diff --git a/pw_bluetooth_sapphire/host/fidl/host_server.cc b/pw_bluetooth_sapphire/host/fidl/host_server.cc
index dd2a108..4460ffa 100644
--- a/pw_bluetooth_sapphire/host/fidl/host_server.cc
+++ b/pw_bluetooth_sapphire/host/fidl/host_server.cc
@@ -523,10 +523,10 @@
     return;
   }
 
-  // TODO(BT-649): Dual-mode currently not supported; if the peer supports
-  // LowEnergy we assume LE. If a dual-mode peer, we should attempt to connect
+  // TODO(fxb/1242): Dual-mode currently not supported; if the peer supports
+  // BR/EDR we prefer BR/EDR. If a dual-mode peer, we should attempt to connect
   // both protocols.
-  if (!peer->le()) {
+  if (peer->bredr()) {
     ConnectBrEdr(id, std::move(callback));
     return;
   }
diff --git a/pw_bluetooth_sapphire/host/fidl/host_server_unittest.cc b/pw_bluetooth_sapphire/host/fidl/host_server_unittest.cc
index 90935b7..4aa0a57 100644
--- a/pw_bluetooth_sapphire/host/fidl/host_server_unittest.cc
+++ b/pw_bluetooth_sapphire/host/fidl/host_server_unittest.cc
@@ -126,24 +126,35 @@
     return pairing_delegate;
   }
 
-  std::tuple<bt::gap::Peer*, FakeChannel*> ConnectFakePeer(bool connect_le = true) {
-    auto device_addr = connect_le ? kLeTestAddr : kBredrTestAddr;
-    auto* peer = adapter()->peer_cache()->NewPeer(device_addr, true);
-    EXPECT_TRUE(peer->temporary());
+  bt::gap::Peer* AddFakePeer(const bt::DeviceAddress& address) {
+    bt::gap::Peer* peer = adapter()->peer_cache()->NewPeer(address, /*connectable=*/true);
+    ZX_ASSERT(peer);
+    ZX_ASSERT(peer->temporary());
+
+    test_device()->AddPeer(std::make_unique<FakePeer>(address));
+
+    return peer;
+  }
+
+  using ConnectResult = fit::result<void, fsys::Error>;
+  std::optional<ConnectResult> ConnectFakePeer(bt::PeerId id) {
+    std::optional<ConnectResult> result;
+    host_client()->Connect(fbt::PeerId{id.value()},
+                           [&](ConnectResult _result) { result = _result; });
+    RunLoopUntilIdle();
+    return result;
+  }
+
+  std::tuple<bt::gap::Peer*, FakeChannel*> CreateAndConnectFakePeer(bool connect_le = true) {
+    auto address = connect_le ? kLeTestAddr : kBredrTestAddr;
+    bt::gap::Peer* peer = AddFakePeer(address);
+
     // This is to capture the channel created during the Connection process
     FakeChannel* fake_chan = nullptr;
     data_domain()->set_channel_callback(
         [&fake_chan](fbl::RefPtr<FakeChannel> new_fake_chan) { fake_chan = new_fake_chan.get(); });
 
-    auto fake_peer = std::make_unique<FakePeer>(device_addr);
-    test_device()->AddPeer(std::move(fake_peer));
-
-    std::optional<fit::result<void, fsys::Error>> connect_result;
-    host_client()->Connect(fbt::PeerId{peer->identifier().value()}, [&](auto result) {
-      ASSERT_FALSE(result.is_err());
-      connect_result = std::move(result);
-    });
-    RunLoopUntilIdle();
+    auto connect_result = ConnectFakePeer(peer->identifier());
 
     if (!connect_result || connect_result->is_error()) {
       peer = nullptr;
@@ -176,7 +187,7 @@
     if (!fake_peer_ || !fake_chan_) {
       ASSERT_FALSE(fake_peer_);
       ASSERT_FALSE(fake_chan_);
-      std::tie(fake_peer_, fake_chan_) = ConnectFakePeer(is_le);
+      std::tie(fake_peer_, fake_chan_) = CreateAndConnectFakePeer(is_le);
       ASSERT_TRUE(fake_peer_);
       ASSERT_TRUE(fake_chan_);
       ASSERT_EQ(bt::gap::Peer::ConnectionState::kConnected, fake_peer_->le()->connection_state());
@@ -565,7 +576,7 @@
 }
 
 TEST_F(FIDL_HostServerTest, InitiateBrEdrPairingLePeerFails) {
-  auto [peer, fake_chan] = ConnectFakePeer();
+  auto [peer, fake_chan] = CreateAndConnectFakePeer();
   ASSERT_TRUE(peer);
   ASSERT_TRUE(fake_chan);
   ASSERT_EQ(bt::gap::Peer::ConnectionState::kConnected, peer->le()->connection_state());
@@ -593,7 +604,8 @@
 }
 
 TEST_F(FIDL_HostServerTest, WatchPeersRepliesOnFirstCallWithExistingPeers) {
-  __UNUSED auto* peer = adapter()->peer_cache()->NewPeer(kLeTestAddr, /*connectable=*/true);
+  __UNUSED bt::gap::Peer* peer =
+      adapter()->peer_cache()->NewPeer(kLeTestAddr, /*connectable=*/true);
   ResetHostServer();
 
   // By default the peer cache contains no entries when HostServer is first constructed. The first
@@ -620,7 +632,7 @@
   ASSERT_FALSE(removed.has_value());
 
   // Adding a new peer should resolve the hanging get.
-  auto* peer = adapter()->peer_cache()->NewPeer(kLeTestAddr, /*connectable=*/true);
+  bt::gap::Peer* peer = adapter()->peer_cache()->NewPeer(kLeTestAddr, /*connectable=*/true);
   ASSERT_TRUE(updated.has_value());
   ASSERT_TRUE(removed.has_value());
   EXPECT_EQ(1u, updated->size());
@@ -651,7 +663,7 @@
   // Add then remove a peer. The watcher should only report the removal.
   bt::PeerId id;
   {
-    auto* peer = adapter()->peer_cache()->NewPeer(kLeTestAddr, /*connectable=*/true);
+    bt::gap::Peer* peer = adapter()->peer_cache()->NewPeer(kLeTestAddr, /*connectable=*/true);
     id = peer->identifier();
 
     // |peer| becomes a dangling pointer after the call to RemoveDisconnectedPeer. We scoped the
@@ -683,5 +695,59 @@
             adapter()->le_connection_manager()->security_mode());
 }
 
+TEST_F(FIDL_HostServerTest, ConnectLowEnergy) {
+  bt::gap::Peer* peer = AddFakePeer(kLeTestAddr);
+  EXPECT_EQ(bt::gap::TechnologyType::kLowEnergy, peer->technology());
+
+  auto result = ConnectFakePeer(peer->identifier());
+  ASSERT_TRUE(result);
+  ASSERT_FALSE(result->is_error());
+
+  EXPECT_FALSE(peer->bredr());
+  ASSERT_TRUE(peer->le());
+  EXPECT_TRUE(peer->le()->connected());
+
+  // bt-host should only attempt to connect the LE transport.
+  EXPECT_EQ(1, test_device()->le_create_connection_command_count());
+  EXPECT_EQ(0, test_device()->acl_create_connection_command_count());
+}
+
+TEST_F(FIDL_HostServerTest, ConnectBredr) {
+  bt::gap::Peer* peer = AddFakePeer(kBredrTestAddr);
+  EXPECT_EQ(bt::gap::TechnologyType::kClassic, peer->technology());
+
+  auto result = ConnectFakePeer(peer->identifier());
+  ASSERT_TRUE(result);
+  ASSERT_FALSE(result->is_error());
+
+  EXPECT_FALSE(peer->le());
+  ASSERT_TRUE(peer->bredr());
+  EXPECT_TRUE(peer->bredr()->connected());
+
+  // bt-host should only attempt to connect the BR/EDR transport.
+  EXPECT_EQ(0, test_device()->le_create_connection_command_count());
+  EXPECT_EQ(1, test_device()->acl_create_connection_command_count());
+}
+
+TEST_F(FIDL_HostServerTest, ConnectDualMode) {
+  // Initialize the peer with data for both transport types.
+  bt::gap::Peer* peer = AddFakePeer(kBredrTestAddr);
+  peer->MutLe();
+  ASSERT_TRUE(peer->le());
+  peer->MutBrEdr();
+  ASSERT_TRUE(peer->bredr());
+  EXPECT_EQ(bt::gap::TechnologyType::kDualMode, peer->technology());
+
+  auto result = ConnectFakePeer(peer->identifier());
+  ASSERT_TRUE(result);
+  ASSERT_FALSE(result->is_error());
+
+  // bt-host should only attempt to connect the BR/EDR transport.
+  EXPECT_TRUE(peer->bredr()->connected());
+  EXPECT_FALSE(peer->le()->connected());
+  EXPECT_EQ(0, test_device()->le_create_connection_command_count());
+  EXPECT_EQ(1, test_device()->acl_create_connection_command_count());
+}
+
 }  // namespace
 }  // namespace bthost
diff --git a/pw_bluetooth_sapphire/host/testing/fake_controller.cc b/pw_bluetooth_sapphire/host/testing/fake_controller.cc
index cf89de7..201e306 100644
--- a/pw_bluetooth_sapphire/host/testing/fake_controller.cc
+++ b/pw_bluetooth_sapphire/host/testing/fake_controller.cc
@@ -563,6 +563,8 @@
 
 void FakeController::OnCreateConnectionCommandReceived(
     const hci::CreateConnectionCommandParams& params) {
+  acl_create_connection_command_count_++;
+
   // Cannot issue this command while a request is already pending.
   if (bredr_connect_pending_) {
     RespondWithCommandStatus(hci::kCreateConnection, hci::StatusCode::kCommandDisallowed);
@@ -660,6 +662,8 @@
 
 void FakeController::OnLECreateConnectionCommandReceived(
     const hci::LECreateConnectionCommandParams& params) {
+  le_create_connection_command_count_++;
+
   // Cannot issue this command while a request is already pending.
   if (le_connect_pending_) {
     RespondWithCommandStatus(hci::kLECreateConnection, hci::StatusCode::kCommandDisallowed);
diff --git a/pw_bluetooth_sapphire/host/testing/fake_controller.h b/pw_bluetooth_sapphire/host/testing/fake_controller.h
index 75cd8f4..cfa219f 100644
--- a/pw_bluetooth_sapphire/host/testing/fake_controller.h
+++ b/pw_bluetooth_sapphire/host/testing/fake_controller.h
@@ -165,6 +165,10 @@
   // is unknown.
   FakePeer* FindPeer(const DeviceAddress& address);
 
+  // Counters for HCI commands received.
+  int le_create_connection_command_count() const { return le_create_connection_command_count_; }
+  int acl_create_connection_command_count() const { return acl_create_connection_command_count_; }
+
   // Sets a callback to be invoked when the the base controller parameters change due to a HCI
   // command. These parameters are:
   //
@@ -464,6 +468,10 @@
   // The set of fake peers that are visible.
   std::unordered_map<DeviceAddress, std::unique_ptr<FakePeer>> peers_;
 
+  // Callbacks and counters that are intended for unit tests.
+  int le_create_connection_command_count_ = 0;
+  int acl_create_connection_command_count_ = 0;
+
   fit::closure controller_parameters_cb_;
   ScanStateCallback scan_state_cb_;
   fit::closure advertising_state_cb_;