Skip to main content

profile_client/
lib.rs

1// Copyright 2021 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
5//! An interface for interacting with the `fuchsia.bluetooth.bredr.Profile` protocol.
6//! This interface provides convenience methods to register service searches and advertisements
7//! using the `Profile` protocol and includes a Stream implementation which can be polled to
8//! receive Profile API updates.
9//!
10//! ### Example Usage:
11//!
12//! // Connect to the `f.b.bredr.Profile` protocol.
13//! let profile_svc = fuchsia_component::client::connect_to_protocol::<ProfileMarker>()?;
14//!
15//! // Create a new `ProfileClient` by registering an advertisement. Register searches.
16//! let svc_defs = vec![..];
17//! let channel_params = ChannelParameters { .. };
18//! let mut profile_client = ProfileClient::advertise(profile_svc, &svc_defs, channel_params)?;
19//! profile_client.add_search(..)?;
20//! profile_client.add_search(..)?;
21//!
22//! // Listen for events from the ProfileClient stream implementation.
23//! while let Some(event) = profile_client.next().await? {
24//!     match event {
25//!         ProfileEvent::PeerConnected { .. } => {} // Do something
26//!         ProfileEvent::SearchResult { .. } => {} // Do something
27//!     }
28//! }
29//!
30
31use fidl::client::QueryResponseFut;
32use fidl::endpoints::create_request_stream;
33use fidl_fuchsia_bluetooth as fidl_bt;
34use fidl_fuchsia_bluetooth_bredr as bredr;
35use fuchsia_bluetooth::types::{Channel, PeerId};
36use futures::FutureExt;
37use futures::stream::{FusedStream, Stream, StreamExt};
38use futures::task::{Context, Poll, Waker};
39use log::trace;
40use std::pin::Pin;
41
42/// Error type used by this library.
43mod error;
44
45pub use crate::error::Error;
46
47pub type Result<T> = std::result::Result<T, Error>;
48
49#[derive(Debug)]
50pub enum ProfileEvent {
51    /// A peer has connected.
52    PeerConnected { id: PeerId, protocol: Vec<bredr::ProtocolDescriptor>, channel: Channel },
53    /// A peer matched one of the search results that was started.
54    SearchResult {
55        id: PeerId,
56        protocol: Option<Vec<bredr::ProtocolDescriptor>>,
57        attributes: Vec<bredr::Attribute>,
58    },
59}
60
61impl ProfileEvent {
62    pub fn peer_id(&self) -> PeerId {
63        match self {
64            Self::PeerConnected { id, .. } => *id,
65            Self::SearchResult { id, .. } => *id,
66        }
67    }
68}
69
70impl TryFrom<bredr::SearchResultsRequest> for ProfileEvent {
71    type Error = Error;
72    fn try_from(value: bredr::SearchResultsRequest) -> Result<Self> {
73        let bredr::SearchResultsRequest::ServiceFound { peer_id, protocol, attributes, responder } =
74            value
75        else {
76            return Err(Error::search_result(fidl::Error::Invalid));
77        };
78        let id: PeerId = peer_id.into();
79        responder.send()?;
80        trace!(id:%, protocol:?, attributes:?; "Profile Search Result");
81        Ok(ProfileEvent::SearchResult { id, protocol, attributes })
82    }
83}
84
85impl TryFrom<bredr::ConnectionReceiverRequest> for ProfileEvent {
86    type Error = Error;
87    fn try_from(value: bredr::ConnectionReceiverRequest) -> Result<Self> {
88        let bredr::ConnectionReceiverRequest::Connected { peer_id, channel, protocol, .. } = value
89        else {
90            return Err(Error::connection_receiver(fidl::Error::Invalid));
91        };
92        let id = peer_id.into();
93        let channel = channel.try_into().map_err(Error::connection_receiver)?;
94        trace!(id:%, protocol:?; "Incoming connection");
95        Ok(ProfileEvent::PeerConnected { id, channel, protocol })
96    }
97}
98
99/// Provides an interface to interact with the `fuchsia.bluetooth.bredr.Profile` protocol.
100///
101/// Currently, this implementation supports a single advertisement and multiple searches.
102/// Search result events can be returned for any of the registered services. In the case of
103/// multiple registered searches, consider using the `profile::find_service_class`
104/// function in the `fuchsia_bluetooth` crate to identify the Service Class of the returned event.
105///
106/// The `ProfileClient` is typically used as a stream of ConnectionReceiver connection requests
107/// and SearchResults events. The stream is considered terminated if the advertisement (if set)
108/// has terminated, the ConnectionReceiver stream associated with the advertisement has terminated,
109/// or if _any_ of the registered searches have terminated.
110///
111/// For information about the Profile API, see the [FIDL Docs](//sdk/fidl/fuchsia.bluetooth.bredr/profile.fidl).
112pub struct ProfileClient {
113    /// The proxy that is used to start new searches and advertise.
114    proxy: bredr::ProfileProxy,
115    /// The result for the advertisement.
116    advertisement: Option<QueryResponseFut<bredr::ProfileAdvertiseResult>>,
117    connection_receiver: Option<bredr::ConnectionReceiverRequestStream>,
118    /// The registered results from the search streams. Polled in order.
119    searches: Vec<bredr::SearchResultsRequestStream>,
120    /// This waker will be woken if a new search is added.
121    stream_waker: Option<Waker>,
122    /// True once any of the searches, or the advertisement, have completed.
123    terminated: bool,
124}
125
126impl ProfileClient {
127    /// Create a new Profile that doesn't advertise any services.
128    pub fn new(proxy: bredr::ProfileProxy) -> Self {
129        Self {
130            proxy,
131            advertisement: None,
132            connection_receiver: None,
133            searches: Vec::new(),
134            stream_waker: None,
135            terminated: false,
136        }
137    }
138
139    /// Create a new Profile that advertises the services in `services`.
140    /// Incoming connections will request the `channel mode` provided.
141    pub fn advertise(
142        proxy: bredr::ProfileProxy,
143        services: Vec<bredr::ServiceDefinition>,
144        channel_params: fidl_bt::ChannelParameters,
145    ) -> Result<Self> {
146        if services.is_empty() {
147            return Ok(Self::new(proxy));
148        }
149        let (connect_client, connection_receiver) = create_request_stream();
150        let advertisement = proxy
151            .advertise(bredr::ProfileAdvertiseRequest {
152                services: Some(services),
153                parameters: Some(channel_params),
154                receiver: Some(connect_client),
155                ..Default::default()
156            })
157            .check()?;
158        Ok(Self {
159            advertisement: Some(advertisement),
160            connection_receiver: Some(connection_receiver),
161            ..Self::new(proxy)
162        })
163    }
164
165    pub fn add_search(
166        &mut self,
167        service_uuid: bredr::ServiceClassProfileIdentifier,
168        attributes: Option<Vec<u16>>,
169    ) -> Result<()> {
170        if self.terminated {
171            return Err(Error::AlreadyTerminated);
172        }
173
174        let (results_client, results_stream) = create_request_stream();
175        self.proxy.search(bredr::ProfileSearchRequest {
176            service_uuid: Some(service_uuid),
177            attr_ids: attributes,
178            results: Some(results_client),
179            ..Default::default()
180        })?;
181        self.searches.push(results_stream);
182
183        if let Some(waker) = self.stream_waker.take() {
184            waker.wake();
185        }
186        Ok(())
187    }
188
189    // TODO(https://fxbug.dev/333456020): Consider adding a shutdown method to revoke the active
190    // advertisement.
191}
192
193impl FusedStream for ProfileClient {
194    fn is_terminated(&self) -> bool {
195        self.terminated
196    }
197}
198
199impl Stream for ProfileClient {
200    type Item = Result<ProfileEvent>;
201
202    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
203        if self.terminated {
204            panic!("Profile polled after terminated");
205        }
206
207        if let Some(advertisement) = self.advertisement.as_mut() {
208            if let Poll::Ready(_result) = advertisement.poll_unpin(cx) {
209                // TODO(https://fxbug.dev/333456020): Consider returning to the client of the
210                // library. Not required by any profiles right now.
211                self.advertisement = None;
212            };
213        }
214
215        if let Some(receiver) = self.connection_receiver.as_mut() {
216            if let Poll::Ready(item) = receiver.poll_next_unpin(cx) {
217                match item {
218                    Some(Ok(request)) => return Poll::Ready(Some(request.try_into())),
219                    Some(Err(e)) => return Poll::Ready(Some(Err(Error::connection_receiver(e)))),
220                    None => {
221                        self.terminated = true;
222                        return Poll::Ready(None);
223                    }
224                };
225            };
226        }
227
228        for search in &mut self.searches {
229            if let Poll::Ready(item) = search.poll_next_unpin(cx) {
230                match item {
231                    Some(Ok(request)) => return Poll::Ready(Some(request.try_into())),
232                    Some(Err(e)) => return Poll::Ready(Some(Err(Error::search_result(e)))),
233                    None => {
234                        self.terminated = true;
235                        return Poll::Ready(None);
236                    }
237                }
238            }
239        }
240
241        // Return pending, store the waker to wake if a new poll target is added.
242        self.stream_waker = Some(cx.waker().clone());
243        Poll::Pending
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use bt_channel_test_support::{Transport, create_test_channels};
251    use fidl::endpoints::create_proxy_and_stream;
252    use fuchsia_async as fasync;
253    use fuchsia_bluetooth::types::Uuid;
254    use futures::Future;
255    use futures_test::task::new_count_waker;
256    use std::pin::pin;
257    use test_case::test_case;
258
259    fn make_profile_service_definition(service_uuid: Uuid) -> bredr::ServiceDefinition {
260        bredr::ServiceDefinition {
261            service_class_uuids: Some(vec![service_uuid.into()]),
262            protocol_descriptor_list: Some(vec![
263                bredr::ProtocolDescriptor {
264                    protocol: Some(bredr::ProtocolIdentifier::L2Cap),
265                    params: Some(vec![bredr::DataElement::Uint16(bredr::PSM_AVDTP)]),
266                    ..Default::default()
267                },
268                bredr::ProtocolDescriptor {
269                    protocol: Some(bredr::ProtocolIdentifier::Avdtp),
270                    params: Some(vec![bredr::DataElement::Uint16(0x0103)]), // Indicate v1.3
271                    ..Default::default()
272                },
273            ]),
274            profile_descriptors: Some(vec![bredr::ProfileDescriptor {
275                profile_id: Some(bredr::ServiceClassProfileIdentifier::AdvancedAudioDistribution),
276                major_version: Some(1),
277                minor_version: Some(2),
278                ..Default::default()
279            }]),
280            ..Default::default()
281        }
282    }
283
284    #[test]
285    fn service_advertisement_result_is_no_op() {
286        let mut exec = fasync::TestExecutor::new();
287        let (proxy, mut profile_stream) = create_proxy_and_stream::<bredr::ProfileMarker>();
288
289        let source_uuid =
290            Uuid::new16(bredr::ServiceClassProfileIdentifier::AudioSource.into_primitive());
291        let defs = vec![make_profile_service_definition(source_uuid)];
292        let channel_params = fidl_bt::ChannelParameters {
293            channel_mode: Some(fidl_bt::ChannelMode::Basic),
294            ..Default::default()
295        };
296
297        let mut profile = ProfileClient::advertise(proxy, defs.clone(), channel_params.clone())
298            .expect("Advertise succeeds");
299
300        let (_connect_proxy, adv_responder) = expect_advertisement_registration(
301            &mut exec,
302            &mut profile_stream,
303            defs,
304            Some(channel_params.into()),
305        );
306
307        {
308            let event_fut = profile.next();
309            let mut event_fut = pin!(event_fut);
310            assert!(exec.run_until_stalled(&mut event_fut).is_pending());
311
312            // The lifetime of the advertisement is not tied to the `Advertise` response. The
313            // `ProfileClient` stream should still be active.
314            adv_responder
315                .send(Ok(&bredr::ProfileAdvertiseResponse::default()))
316                .expect("able to respond");
317
318            match exec.run_until_stalled(&mut event_fut) {
319                Poll::Pending => {}
320                x => panic!("Expected pending but got {x:?}"),
321            };
322        }
323
324        assert!(!profile.is_terminated());
325    }
326
327    #[test_case(Transport::Socket ; "socket")]
328    #[test_case(Transport::Fidl ; "fidl")]
329    #[fuchsia::test]
330    fn connection_request_relayed_to_stream(transport: Transport) {
331        let mut exec = fasync::TestExecutor::new();
332        let (proxy, mut profile_stream) = create_proxy_and_stream::<bredr::ProfileMarker>();
333
334        let source_uuid =
335            Uuid::new16(bredr::ServiceClassProfileIdentifier::AudioSource.into_primitive());
336        let defs = vec![make_profile_service_definition(source_uuid)];
337        let channel_params = fidl_bt::ChannelParameters {
338            channel_mode: Some(fidl_bt::ChannelMode::Basic),
339            ..Default::default()
340        };
341
342        let mut profile = ProfileClient::advertise(proxy, defs.clone(), channel_params.clone())
343            .expect("Advertise succeeds");
344
345        let (connect_proxy, _adv_responder) = expect_advertisement_registration(
346            &mut exec,
347            &mut profile_stream,
348            defs,
349            Some(channel_params.into()),
350        );
351
352        let remote_peer = PeerId(12343);
353        {
354            let event_fut = profile.next();
355            let mut event_fut = pin!(event_fut);
356            assert!(exec.run_until_stalled(&mut event_fut).is_pending());
357
358            let (remote_chan, _local) = create_test_channels(transport);
359            connect_proxy
360                .connected(&remote_peer.into(), bredr::Channel::try_from(remote_chan).unwrap(), &[])
361                .expect("connection should work");
362
363            match exec.run_until_stalled(&mut event_fut) {
364                Poll::Ready(Some(Ok(ProfileEvent::PeerConnected { id, .. }))) => {
365                    assert_eq!(id, remote_peer);
366                }
367                x => panic!("Expected an error from the advertisement, got {:?}", x),
368            };
369        }
370
371        // Stream should error and terminate when the advertisement is disconnected.
372        drop(connect_proxy);
373
374        match exec.run_until_stalled(&mut profile.next()) {
375            Poll::Ready(None) => {}
376            x => panic!("Expected profile to end on advertisement drop, got {:?}", x),
377        };
378
379        assert!(profile.is_terminated());
380    }
381
382    #[track_caller]
383    fn expect_advertisement_registration(
384        exec: &mut fasync::TestExecutor,
385        profile_stream: &mut bredr::ProfileRequestStream,
386        expected_defs: Vec<bredr::ServiceDefinition>,
387        expected_params: Option<fidl_bt::ChannelParameters>,
388    ) -> (bredr::ConnectionReceiverProxy, bredr::ProfileAdvertiseResponder) {
389        match exec.run_until_stalled(&mut profile_stream.next()) {
390            Poll::Ready(Some(Ok(bredr::ProfileRequest::Advertise { payload, responder }))) => {
391                assert!(payload.services.is_some());
392                assert_eq!(payload.services.unwrap(), expected_defs);
393                assert_eq!(payload.parameters, expected_params);
394                assert!(payload.receiver.is_some());
395                (payload.receiver.unwrap().into_proxy(), responder)
396            }
397            x => panic!("Expected ready advertisement request, got {:?}", x),
398        }
399    }
400
401    #[track_caller]
402    fn expect_search_registration(
403        exec: &mut fasync::TestExecutor,
404        profile_stream: &mut bredr::ProfileRequestStream,
405        search_uuid: bredr::ServiceClassProfileIdentifier,
406        search_attrs: &[u16],
407    ) -> bredr::SearchResultsProxy {
408        match exec.run_until_stalled(&mut profile_stream.next()) {
409            Poll::Ready(Some(Ok(bredr::ProfileRequest::Search { payload, .. }))) => {
410                let bredr::ProfileSearchRequest {
411                    service_uuid: Some(service_uuid),
412                    attr_ids,
413                    results: Some(results),
414                    ..
415                } = payload
416                else {
417                    panic!("invalid parameters");
418                };
419                let attr_ids = attr_ids.unwrap_or_default();
420                assert_eq!(&attr_ids[..], search_attrs);
421                assert_eq!(service_uuid, search_uuid);
422                results.into_proxy()
423            }
424            x => panic!("Expected ready request for a search, got: {:?}", x),
425        }
426    }
427
428    #[test]
429    fn responds_to_search_results() {
430        let mut exec = fasync::TestExecutor::new();
431        let (proxy, mut profile_stream) = create_proxy_and_stream::<bredr::ProfileMarker>();
432
433        let mut profile = ProfileClient::new(proxy);
434
435        let search_attrs = vec![bredr::ATTR_BLUETOOTH_PROFILE_DESCRIPTOR_LIST];
436
437        let source_uuid = bredr::ServiceClassProfileIdentifier::AudioSource;
438        profile
439            .add_search(source_uuid, Some(search_attrs.clone()))
440            .expect("adding search succeeds");
441
442        let sink_uuid = bredr::ServiceClassProfileIdentifier::AudioSink;
443        profile.add_search(sink_uuid, Some(search_attrs.clone())).expect("adding search succeeds");
444
445        // Get the search clients out
446        let source_results_proxy = expect_search_registration(
447            &mut exec,
448            &mut profile_stream,
449            source_uuid,
450            &search_attrs[..],
451        );
452        let sink_results_proxy = expect_search_registration(
453            &mut exec,
454            &mut profile_stream,
455            sink_uuid,
456            &search_attrs[..],
457        );
458
459        // Send a search request, process the request (by polling event stream) and confirm it responds.
460
461        // Report a search result, which should be replied to.
462        let attributes = &[];
463        let found_peer_id = PeerId(1);
464        let results_fut =
465            source_results_proxy.service_found(&found_peer_id.into(), None, attributes);
466        let mut results_fut = pin!(results_fut);
467
468        match exec.run_until_stalled(&mut profile.next()) {
469            Poll::Ready(Some(Ok(ProfileEvent::SearchResult { id, .. }))) => {
470                assert_eq!(found_peer_id, id);
471            }
472            x => panic!("Expected search result to be ready: {:?}", x),
473        }
474
475        match exec.run_until_stalled(&mut results_fut) {
476            Poll::Ready(Ok(())) => {}
477            x => panic!("Expected a response from the source result, got {:?}", x),
478        };
479
480        let results_fut = sink_results_proxy.service_found(&found_peer_id.into(), None, attributes);
481        let mut results_fut = pin!(results_fut);
482
483        match exec.run_until_stalled(&mut profile.next()) {
484            Poll::Ready(Some(Ok(ProfileEvent::SearchResult { id, .. }))) => {
485                assert_eq!(found_peer_id, id);
486            }
487            x => panic!("Expected search result to be ready: {:?}", x),
488        }
489
490        match exec.run_until_stalled(&mut results_fut) {
491            Poll::Ready(Ok(())) => {}
492            x => panic!("Expected a response from the sink result, got {:?}", x),
493        };
494
495        // Stream should error and terminate when one of the result streams is disconnected.
496        drop(source_results_proxy);
497
498        match exec.run_until_stalled(&mut profile.next()) {
499            Poll::Ready(None) => {}
500            x => panic!("Expected profile to end on search result drop, got {:?}", x),
501        };
502
503        assert!(profile.is_terminated());
504
505        // Adding a search after termination should fail.
506        assert!(profile.add_search(sink_uuid, None).is_err());
507    }
508
509    #[test]
510    fn waker_gets_awoken_when_search_added() {
511        let mut exec = fasync::TestExecutor::new();
512        let (proxy, mut profile_stream) = create_proxy_and_stream::<bredr::ProfileMarker>();
513
514        let mut profile = ProfileClient::new(proxy);
515
516        // Polling the ProfileClient stream before any poll targets have been added should save
517        // a waker to be awoken when a new search is added.
518        let profile_fut = profile.next();
519
520        let (waker, profile_fut_wake_count) = new_count_waker();
521        let mut counting_ctx = Context::from_waker(&waker);
522
523        let profile_fut = pin!(profile_fut);
524        assert!(profile_fut.poll(&mut counting_ctx).is_pending());
525
526        // Since there are no poll targets, save the initial count. We expect this count
527        // to change when a new poll target is added.
528        let initial_count = profile_fut_wake_count.get();
529
530        // Adding a search should be OK. We expect to get the search request and the
531        // waker should be awoken.
532        let source_uuid = bredr::ServiceClassProfileIdentifier::AudioSource;
533        profile.add_search(source_uuid, None).expect("adding search succeeds");
534        let search_proxy =
535            expect_search_registration(&mut exec, &mut profile_stream, source_uuid, &[]);
536
537        // Since we've added a search, we expect the wake count to increase by one.
538        let after_search_count = profile_fut_wake_count.get();
539        assert_eq!(after_search_count, initial_count + 1);
540
541        // Reporting a search result should work as intended. The stream should produce an event.
542        let attributes = &[];
543        let found_peer_id = PeerId(123);
544        let results_fut = search_proxy.service_found(&found_peer_id.into(), None, attributes);
545        let mut results_fut = pin!(results_fut);
546
547        match exec.run_until_stalled(&mut profile.next()) {
548            Poll::Ready(Some(Ok(ProfileEvent::SearchResult { id, .. }))) => {
549                assert_eq!(found_peer_id, id);
550            }
551            x => panic!("Expected search result to be ready: {:?}", x),
552        }
553
554        match exec.run_until_stalled(&mut results_fut) {
555            Poll::Ready(Ok(())) => {}
556            x => panic!("Expected a response from the source result, got {:?}", x),
557        };
558    }
559}