[bt][bt-gap] RestoreBonds in chunks of 16

When invoking fuchsia.bluetooth.host.Host/RestoreBonds, bt-gap will use
at most MAX_BONDS_PER_RESTORE_BONDS bonds in each successive call until
all bonds are restored from secure stash. This prevents any call from
exceeding message size limits.

Bug: fxbug.dev/80564
Test: fx test -o bt-gap-unittests (new host_device_restore_bonds test)
      Manually reboot astro using MAX_BONDS_PER_RESTORE_BONDS = 2 and
      observe that all 5 device bonds are restored over 3 calls

Change-Id: Iecf4cd825d9966c5abc139dd0b5ad4e421f5367b
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/559781
Commit-Queue: Xo Wang <xow@google.com>
Reviewed-by: Jeff Belgum <belgum@google.com>
diff --git a/src/connectivity/bluetooth/core/bt-gap/BUILD.gn b/src/connectivity/bluetooth/core/bt-gap/BUILD.gn
index afaa1dd..6ffe4a2 100644
--- a/src/connectivity/bluetooth/core/bt-gap/BUILD.gn
+++ b/src/connectivity/bluetooth/core/bt-gap/BUILD.gn
@@ -79,6 +79,8 @@
     "src/watch_peers.rs",
   ]
 
+  test_deps = [ "//src/lib/test_util" ]
+
   configs -= [ "//build/config/rust:allow_unused_results" ]
 }
 
diff --git a/src/connectivity/bluetooth/core/bt-gap/src/host_device.rs b/src/connectivity/bluetooth/core/bt-gap/src/host_device.rs
index acb3330..2c8d657 100644
--- a/src/connectivity/bluetooth/core/bt-gap/src/host_device.rs
+++ b/src/connectivity/bluetooth/core/bt-gap/src/host_device.rs
@@ -11,7 +11,7 @@
         inspect::Inspectable,
         types::{Address, BondingData, HostData, HostId, HostInfo, Peer, PeerId},
     },
-    futures::{Future, FutureExt, StreamExt, TryFutureExt},
+    futures::{future::try_join_all, Future, FutureExt, StreamExt, TryFutureExt},
     log::{error, info, trace, warn},
     parking_lot::RwLock,
     pin_utils::pin_mut,
@@ -177,10 +177,25 @@
         &self,
         bonds: Vec<BondingData>,
     ) -> impl Future<Output = types::Result<Vec<sys::BondingData>>> {
-        self.0
-            .proxy
-            .restore_bonds(&mut bonds.into_iter().map(sys::BondingData::from))
-            .map_err(|e| e.into())
+        // TODO(fxbug.dev/80564): Due to the maximum message size, the RestoreBonds call has an
+        // upper limit on the number of bonds that may be restored at once. However, this limit is
+        // based on the complexity of fields packed into sys::BondingData, which can be measured
+        // dynamically with measure-tape as the vector is built. Maximizing the number of bonds per
+        // call may improve performance. Instead, a conservative fixed maximum is chosen here (at
+        // this time, the maximum message size is 64 KiB and a fully-populated BondingData without
+        // peer service records is close to 700 B).
+        const MAX_BONDS_PER_RESTORE_BONDS: usize = 16;
+        let bonds_chunks = bonds.chunks(MAX_BONDS_PER_RESTORE_BONDS);
+
+        // Bonds that can't be restored are sent back in Ok(Vec<_>), which would not cause
+        // try_join_all to bail early
+        try_join_all(bonds_chunks.map(|c| {
+            self.0
+                .proxy
+                .restore_bonds(&mut c.into_iter().cloned().map(sys::BondingData::from))
+                .map_err(|e| e.into())
+        }))
+        .map_ok(|v| v.into_iter().flatten().collect())
     }
 
     pub fn set_connectable(&self, value: bool) -> impl Future<Output = types::Result<()>> {
diff --git a/src/connectivity/bluetooth/core/bt-gap/src/test/host_device.rs b/src/connectivity/bluetooth/core/bt-gap/src/test/host_device.rs
index 031dd24..9fd3ab0 100644
--- a/src/connectivity/bluetooth/core/bt-gap/src/test/host_device.rs
+++ b/src/connectivity/bluetooth/core/bt-gap/src/test/host_device.rs
@@ -7,10 +7,13 @@
     fidl::endpoints::RequestStream,
     fidl_fuchsia_bluetooth_host::{HostControlHandle, HostMarker, HostRequest, HostRequestStream},
     fidl_fuchsia_bluetooth_sys::HostInfo as FidlHostInfo,
-    fuchsia_bluetooth::types::{Address, BondingData, HostId, HostInfo, Peer, PeerId},
+    fuchsia_bluetooth::types::{
+        bonding_data::example, Address, BondingData, HostId, HostInfo, Peer, PeerId,
+    },
     futures::{future, join, stream::StreamExt},
     parking_lot::RwLock,
     std::{path::Path, sync::Arc},
+    test_util::assert_gt,
 };
 
 use crate::host_device::{HostDevice, HostListener};
@@ -118,6 +121,42 @@
     Ok(())
 }
 
+// Create a HostDevice with a fake channel, restore bonds, check that the FIDL method is called
+// over multiple paginated iterations
+#[fuchsia_async::run_until_stalled(test)]
+async fn host_device_restore_bonds() -> Result<(), Error> {
+    let (client, server) = fidl::endpoints::create_proxy_and_stream::<HostMarker>()?;
+    let address = Address::Public([0, 0, 0, 0, 0, 0]);
+    let host = HostDevice::mock(HostId(1), address, Path::new("/dev/class/bt-host/test"), client);
+
+    // TODO(fxbug.dev/80564): Assume that 256 bonds is enough to cause HostDevice to use multiple
+    // FIDL calls to transmit all of the bonds (at this time, the maximum message size is 64 KiB
+    // and a fully-populated BondingData without peer service records is close to 700 B).
+    let bonds = (0..=255).map(|i| {
+        example::bond(Address::Public([i, i, i, i, i, i]), Address::Public([i, i, i, i, i, i]))
+    });
+    let restore_bonds = host.restore_bonds(bonds.collect());
+
+    // Create testing Host server that responds to each request with a completed future until
+    // restore_bonds yields a result
+    let mut num_restore_bonds_calls: i32 = 0;
+    let run_host = server.take_until(restore_bonds).for_each(|request| {
+        match request {
+            Ok(HostRequest::RestoreBonds { responder, .. }) => {
+                // Succeed always by not sending back any rejected bonds
+                let _ = responder.send(&mut std::iter::empty());
+                num_restore_bonds_calls += 1;
+            }
+            x => panic!("Expected RestoreBonds Request but got: {:?}", x),
+        }
+        future::ready(())
+    });
+
+    let _ = run_host.await;
+    assert_gt!(num_restore_bonds_calls, 1);
+    Ok(())
+}
+
 // TODO(fxbug.dev/39373): Add host.fidl emulation to bt-fidl-mocks and use that instead.
 async fn expect_call<F>(stream: Arc<RwLock<HostRequestStream>>, f: F) -> Result<(), Error>
 where