Skip to main content

host_watcher/
lib.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use async_utils::hanging_get::client::HangingGetStream;
6use core::pin::Pin;
7use core::task::{Context, Poll};
8use fidl_fuchsia_bluetooth_sys as sys;
9use fuchsia_bluetooth::Error;
10use fuchsia_bluetooth::types::{Address, HostInfo};
11use futures::ready;
12use futures::stream::{FusedStream, Stream, StreamExt};
13use log::trace;
14
15/// Item type returned by `<HostWatcher as Stream>::poll_next`.
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub enum HostEvent {
18    /// The existing active host changed discoverable state.
19    Discoverable(bool),
20    /// There is a new active host.
21    NewActiveHost { discoverable: bool },
22    /// There is no active host.
23    NotAvailable,
24}
25
26/// The termination status of the stream.
27#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
28enum Terminated {
29    Active,
30    ShuttingDown,
31    Done,
32}
33
34/// Watches for changes in state in the active Bluetooth Host of the system.
35/// `HostWatcher` implements Stream. The `HostWatcher` _must_ be polled to receive updates about
36/// the currently active host.
37pub struct HostWatcher {
38    /// Hanging-get client wrapper to watch for changes in Host state.
39    host_updates: HangingGetStream<sys::HostWatcherProxy, Vec<sys::HostInfo>>,
40    /// Information about the currently active Host, or None if there is no such Host.
41    active_host: Option<HostInfo>,
42    /// Termination status of the `host_updates` watcher.
43    terminated: Terminated,
44}
45
46impl HostWatcher {
47    /// Returns a HostWatcher that watches for changes in state of the currently active Bluetooth
48    /// Host.
49    pub fn new(host_watcher: sys::HostWatcherProxy) -> Self {
50        let host_updates =
51            HangingGetStream::new_with_fn_ptr(host_watcher, sys::HostWatcherProxy::watch);
52        Self { host_updates, active_host: None, terminated: Terminated::Active }
53    }
54
55    #[cfg(test)]
56    fn new_with_state(host_watcher: sys::HostWatcherProxy, active_host: Option<HostInfo>) -> Self {
57        let mut this = Self::new(host_watcher);
58        this.active_host = active_host;
59        this
60    }
61
62    pub fn set_active_host(&mut self, host: HostInfo) {
63        self.active_host = Some(host);
64    }
65
66    // Compares the `new` host state to the current and returns a HostEvent if the relevant state
67    // has changed.
68    fn compare(&self, new: &Option<HostInfo>) -> Option<HostEvent> {
69        trace!("Current ({:?}) - New ({:?})", self.active_host, new);
70        match (&self.active_host, new) {
71            (None, Some(info)) => {
72                Some(HostEvent::NewActiveHost { discoverable: info.discoverable })
73            }
74            (Some(_), None) => Some(HostEvent::NotAvailable),
75            (Some(current_info), Some(new_info)) if current_info.id != new_info.id => {
76                Some(HostEvent::NewActiveHost { discoverable: new_info.discoverable })
77            }
78            (Some(current_info), Some(new_info))
79                if current_info.discoverable != new_info.discoverable =>
80            {
81                // The host discoverable state changed.
82                Some(HostEvent::Discoverable(new_info.discoverable))
83            }
84            _ => None, // Otherwise, there was no change in host availability or state.
85        }
86    }
87
88    fn handle_host_watcher_update(
89        &mut self,
90        update: Vec<sys::HostInfo>,
91    ) -> Result<Option<HostEvent>, Error> {
92        let maybe_active = update
93            .iter()
94            .find(|info| info.active.unwrap_or_default())
95            .map(HostInfo::try_from)
96            .transpose()?;
97
98        let event = self.compare(&maybe_active);
99        self.active_host = maybe_active;
100        return Ok(event);
101    }
102
103    /// Returns all the known addresses of the active Host, or None if not set.
104    pub fn addresses(&self) -> Option<Vec<Address>> {
105        self.active_host.as_ref().map(|host| host.addresses.clone())
106    }
107
108    /// Returns the public address of the active Host, or None if not set.
109    pub fn public_address(&self) -> Option<Address> {
110        self.active_host
111            .as_ref()
112            .map(|host| {
113                host.addresses.iter().find(|addr| matches!(addr, Address::Public(_))).copied()
114            })
115            .flatten()
116    }
117
118    /// Returns the BLE address of the active Host, or None if not set.
119    pub fn ble_address(&self) -> Option<Address> {
120        self.addresses()
121            .map(|addrs| addrs.into_iter().find(|addr| matches!(addr, Address::Random(_))))
122            .flatten()
123            .or_else(|| self.public_address())
124    }
125
126    /// Returns true if `address_bytes` matches any of the known addresses of the active Host.
127    /// Returns false if there is no active Host.
128    pub fn is_valid_address(&self, address_bytes: &[u8; 6]) -> bool {
129        self.active_host
130            .as_ref()
131            .is_some_and(|host| host.addresses.iter().any(|addr| addr.bytes() == address_bytes))
132    }
133
134    /// Returns the current discoverable state of the active Host, or None if not set.
135    pub fn pairing_mode(&self) -> Option<bool> {
136        self.active_host.as_ref().map(|h| h.discoverable)
137    }
138
139    pub fn local_name(&self) -> Option<String> {
140        self.active_host.as_ref().map(|h| h.local_name.clone()).flatten()
141    }
142}
143
144impl Stream for HostWatcher {
145    type Item = Result<HostEvent, Error>;
146
147    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
148        match self.terminated {
149            Terminated::Active => {}
150            Terminated::ShuttingDown => {
151                self.terminated = Terminated::Done;
152                return Poll::Ready(None);
153            }
154            Terminated::Done => panic!("Cannot poll a terminated stream"),
155        }
156
157        // Keep polling the request stream until it produces a request that should be returned or it
158        // produces Poll::Pending.
159        loop {
160            let result = ready!(self.host_updates.poll_next_unpin(cx));
161
162            let result = match result {
163                Some(Ok(update)) => match self.handle_host_watcher_update(update) {
164                    Ok(None) => continue,
165                    Ok(Some(request)) => Some(Ok(request)),
166                    Err(e) => Some(Err(e)),
167                },
168                Some(Err(e)) => {
169                    // FIDL errors are typically irrecoverable - return the Error and stage stream
170                    // for termination. The next time it is polled, it will complete.
171                    self.terminated = Terminated::ShuttingDown;
172                    Some(Err(e.into()))
173                }
174                None => None,
175            };
176            if result.is_none() {
177                trace!("HostWatcher hanging-get exhausted");
178                self.terminated = Terminated::Done;
179            }
180
181            return Poll::Ready(result);
182        }
183    }
184}
185
186impl FusedStream for HostWatcher {
187    fn is_terminated(&self) -> bool {
188        self.terminated == Terminated::Done
189    }
190}
191
192#[cfg(test)]
193pub(crate) mod tests {
194    use super::*;
195
196    use assert_matches::assert_matches;
197    use async_utils::PollExt;
198    use fuchsia_async as fasync;
199    use fuchsia_bluetooth::types::{HostId, example_host};
200    use std::pin::pin;
201
202    #[track_caller]
203    fn expect_watch_request(
204        exec: &mut fasync::TestExecutor,
205        stream: &mut sys::HostWatcherRequestStream,
206    ) -> sys::HostWatcherWatchResponder {
207        let expect_fut = stream.select_next_some();
208        let mut expect_fut = pin!(expect_fut);
209        exec.run_until_stalled(&mut expect_fut)
210            .expect("ready")
211            .expect("valid FIDL request")
212            .into_watch()
213            .expect("Watch request")
214    }
215
216    #[fuchsia::test]
217    fn is_valid_address_matches_active_host_addresses() {
218        let _exec = fasync::TestExecutor::new();
219
220        let (proxy, _server) = fidl::endpoints::create_proxy_and_stream::<sys::HostWatcherMarker>();
221        let mut watcher = HostWatcher::new(proxy);
222
223        let public = Address::Public([1, 2, 3, 4, 5, 6]);
224        let random = Address::Random([2, 3, 2, 3, 2, 3]);
225        let unknown = [9, 9, 9, 9, 9, 9];
226
227        // No active host - no address is valid.
228        assert!(!watcher.is_valid_address(public.bytes()));
229
230        // Active host with a public and a random address.
231        let mut host =
232            example_host(HostId(1), /* active= */ true, /* discoverable= */ true);
233        host.addresses.as_mut().unwrap().push(random.into());
234        watcher.set_active_host(host.try_into().unwrap());
235
236        assert!(watcher.is_valid_address(public.bytes()));
237        assert!(watcher.is_valid_address(random.bytes()));
238        assert!(!watcher.is_valid_address(&unknown));
239    }
240
241    #[fuchsia::test]
242    fn update_with_no_hosts_stream_is_pending() {
243        let mut exec = fasync::TestExecutor::new();
244
245        let (proxy, mut server) =
246            fidl::endpoints::create_proxy_and_stream::<sys::HostWatcherMarker>();
247        let mut watcher = HostWatcher::new(proxy);
248
249        let _ = exec.run_until_stalled(&mut watcher.next()).expect_pending("No updates");
250
251        let watch_responder = expect_watch_request(&mut exec, &mut server);
252        // Respond with no hosts.
253        let _ = watch_responder.send(&[]).unwrap();
254
255        // By default, there are no hosts, so when the upstream watcher responds with no hosts, the
256        // watcher stream should not yield an event.
257        let _ = exec.run_until_stalled(&mut watcher.next()).expect_pending("No updates");
258    }
259
260    #[fuchsia::test]
261    fn update_with_active_host_change_yields_items() {
262        let mut exec = fasync::TestExecutor::new();
263
264        let (proxy, mut server) =
265            fidl::endpoints::create_proxy_and_stream::<sys::HostWatcherMarker>();
266        let mut watcher = HostWatcher::new(proxy);
267
268        let _ = exec.run_until_stalled(&mut watcher.next()).expect_pending("No updates");
269
270        let watch_responder = expect_watch_request(&mut exec, &mut server);
271        // Respond with an active host.
272        let host1 =
273            example_host(HostId(1), /* active= */ true, /* discoverable= */ false);
274        let _ = watch_responder.send(&[host1]).unwrap();
275
276        // HostWatcher stream should yield a change in host state.
277        let item = exec.run_until_stalled(&mut watcher.next()).expect("host update ready");
278        assert_matches!(item, Some(Ok(HostEvent::NewActiveHost { discoverable: false })));
279
280        // Because this is a hanging-get, we expect the HostWatcher to make the next request.
281        let _ = exec.run_until_stalled(&mut watcher.next()).expect_pending("No updates");
282        let watch_responder = expect_watch_request(&mut exec, &mut server);
283        // Respond with no hosts.
284        let _ = watch_responder.send(&[]).unwrap();
285
286        // HostWatcher stream should yield a change in host state.
287        let item = exec.run_until_stalled(&mut watcher.next()).expect("host update");
288        assert_matches!(item, Some(Ok(HostEvent::NotAvailable)));
289    }
290
291    #[fuchsia::test]
292    fn active_to_no_active_host_update_yields_event() {
293        let mut exec = fasync::TestExecutor::new();
294
295        let (proxy, mut server) =
296            fidl::endpoints::create_proxy_and_stream::<sys::HostWatcherMarker>();
297        let host = example_host(HostId(1), /* active= */ true, /* discoverable= */ false);
298        // HostWatcher starts off with a tracked active, non-discoverable host.
299        let mut watcher = HostWatcher::new_with_state(proxy, host.try_into().ok());
300        let _ = exec.run_until_stalled(&mut watcher.next()).expect_pending("No updates");
301
302        let watch_responder = expect_watch_request(&mut exec, &mut server);
303        // Respond with no active hosts.
304        let host2 =
305            example_host(HostId(2), /* active= */ false, /* discoverable= */ false);
306        let host3 =
307            example_host(HostId(3), /* active= */ false, /* discoverable= */ false);
308        let _ = watch_responder.send(&[host2, host3]).unwrap();
309
310        // HostWatcher stream should yield a change in host state since it went from active host
311        // to no active host.
312        let item = exec.run_until_stalled(&mut watcher.next()).expect("host update");
313        assert_matches!(item, Some(Ok(HostEvent::NotAvailable)));
314    }
315
316    #[fuchsia::test]
317    fn update_with_no_active_host_changes_is_pending() {
318        let mut exec = fasync::TestExecutor::new();
319
320        let (proxy, mut server) =
321            fidl::endpoints::create_proxy_and_stream::<sys::HostWatcherMarker>();
322        // HostWatcher starts off with a tracked active, non-discoverable host.
323        let mut host1 =
324            example_host(HostId(1), /* active= */ true, /* discoverable= */ false);
325        let mut watcher = HostWatcher::new_with_state(proxy, host1.clone().try_into().ok());
326        let _ = exec.run_until_stalled(&mut watcher.next()).expect_pending("No updates");
327
328        let watch_responder = expect_watch_request(&mut exec, &mut server);
329        // Respond with the same active host, and a different inactive host.
330        let host2 =
331            example_host(HostId(2), /* active= */ false, /* discoverable= */ false);
332        let _ = watch_responder.send(&[host1.clone(), host2]).unwrap();
333
334        // No HostWatcher stream item because the active host has not changed.
335        let _ = exec.run_until_stalled(&mut watcher.next()).expect_pending("No updates");
336
337        let watch_responder = expect_watch_request(&mut exec, &mut server);
338        // Same active host changes - but not in a relevant way.
339        host1.discovering = Some(false);
340        host1.local_name = Some("123".to_string());
341        let _ = watch_responder.send(&[host1]).unwrap();
342
343        // No HostWatcher stream item because the discoverable of the active host hasn't changed.
344        let _ = exec.run_until_stalled(&mut watcher.next()).expect_pending("No updates");
345    }
346
347    #[fuchsia::test]
348    fn update_with_active_host_discoverable_change_yields_item() {
349        let mut exec = fasync::TestExecutor::new();
350
351        let (proxy, mut server) =
352            fidl::endpoints::create_proxy_and_stream::<sys::HostWatcherMarker>();
353        let mut host1 =
354            example_host(HostId(1), /* active= */ true, /* discoverable= */ false);
355        // HostWatcher starts off with a tracked active, non-discoverable host.
356        let mut watcher = HostWatcher::new_with_state(proxy, host1.clone().try_into().ok());
357        let _ = exec.run_until_stalled(&mut watcher.next()).expect_pending("No updates");
358
359        let watch_responder = expect_watch_request(&mut exec, &mut server);
360        // The same active host becomes discoverable.
361        host1.discoverable = Some(true);
362        let _ = watch_responder.send(&[host1.clone()]).unwrap();
363
364        let item = exec.run_until_stalled(&mut watcher.next()).expect("host update");
365        assert_matches!(item, Some(Ok(HostEvent::Discoverable(true))));
366    }
367
368    #[fuchsia::test]
369    fn update_with_new_active_host_yields_item() {
370        let mut exec = fasync::TestExecutor::new();
371
372        let (proxy, mut server) =
373            fidl::endpoints::create_proxy_and_stream::<sys::HostWatcherMarker>();
374        let host1 =
375            example_host(HostId(1), /* active= */ true, /* discoverable= */ false);
376        // HostWatcher starts off with a tracked active, non-discoverable host.
377        let mut watcher = HostWatcher::new_with_state(proxy, host1.clone().try_into().ok());
378        let _ = exec.run_until_stalled(&mut watcher.next()).expect_pending("No updates");
379
380        // Receive an update about a new active, non-discoverable host.
381        let watch_responder = expect_watch_request(&mut exec, &mut server);
382        let host2 =
383            example_host(HostId(2), /* active= */ true, /* discoverable= */ false);
384        let _ = watch_responder.send(&[host2]).unwrap();
385
386        let item = exec.run_until_stalled(&mut watcher.next()).expect("host update");
387        assert_matches!(item, Some(Ok(HostEvent::NewActiveHost { discoverable: false })));
388
389        // Receive an update about a new active, discoverable host.
390        let _ = exec.run_until_stalled(&mut watcher.next()).expect_pending("No updates");
391        let watch_responder = expect_watch_request(&mut exec, &mut server);
392        let host3 = example_host(HostId(3), /* active= */ true, /* discoverable= */ true);
393        let _ = watch_responder.send(&[host3]).unwrap();
394
395        let item = exec.run_until_stalled(&mut watcher.next()).expect("host update");
396        assert_matches!(item, Some(Ok(HostEvent::NewActiveHost { discoverable: true })));
397    }
398
399    #[fuchsia::test]
400    fn invalidly_formatted_host_returns_error_stream_item() {
401        let mut exec = fasync::TestExecutor::new();
402
403        let (proxy, mut server) =
404            fidl::endpoints::create_proxy_and_stream::<sys::HostWatcherMarker>();
405        let mut watcher = HostWatcher::new(proxy);
406        let _ = exec.run_until_stalled(&mut watcher.next()).expect_pending("No updates");
407        assert!(!watcher.is_terminated());
408
409        let watch_responder = expect_watch_request(&mut exec, &mut server);
410        // HostInfo is missing a bunch of mandatory fields.
411        let invalid_host =
412            sys::HostInfo { id: Some(HostId(12).into()), active: Some(true), ..Default::default() };
413        let _ = watch_responder.send(&[invalid_host]).unwrap();
414
415        let item = exec.run_until_stalled(&mut watcher.next()).expect("host watcher update");
416        assert_matches!(item, Some(Err(_)));
417        assert!(!watcher.is_terminated());
418    }
419
420    #[fuchsia::test]
421    fn closing_fidl_server_terminates_host_watcher() {
422        let mut exec = fasync::TestExecutor::new();
423
424        let (proxy, mut server) =
425            fidl::endpoints::create_proxy_and_stream::<sys::HostWatcherMarker>();
426        let mut watcher = HostWatcher::new(proxy);
427        let _ = exec.run_until_stalled(&mut watcher.next()).expect_pending("No updates");
428        assert!(!watcher.is_terminated());
429
430        let watch_responder = expect_watch_request(&mut exec, &mut server);
431        let _ = watch_responder.send(&[]).unwrap();
432
433        // The upstream `HostWatcher` protocol server disconnects. This should result in a FIDL
434        // error propagated to the stream. The next time the `HostWatcher` stream is polled, it
435        // should detect closure and terminate.
436        drop(server);
437
438        let item = exec.run_until_stalled(&mut watcher.next()).expect("host watcher FIDL error");
439        assert_matches!(item, Some(Err(_)));
440        assert!(!watcher.is_terminated());
441
442        let item = exec.run_until_stalled(&mut watcher.next()).expect("host watcher termination");
443        assert_matches!(item, None);
444        assert!(watcher.is_terminated());
445    }
446}