Skip to main content

fuchsia_bluetooth/types/
channel.rs

1// Copyright 2026 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 fidl::endpoints::{ClientEnd, Proxy};
6use fidl_fuchsia_bluetooth as fidl_bt;
7use fidl_fuchsia_bluetooth_bredr as bredr;
8use fuchsia_sync::Mutex;
9use futures::sink::Sink;
10use futures::stream::{FusedStream, Stream};
11use futures::{Future, StreamExt};
12use log::warn;
13use std::fmt;
14use std::pin::Pin;
15use std::sync::Arc;
16use std::task::{Context, Poll};
17
18use crate::error::Error;
19
20pub mod fidl_client;
21pub mod fidl_server;
22pub mod socket;
23
24use fidl_client::FidlClientConnection;
25use fidl_server::FidlServerConnection;
26use socket::SocketConnection;
27
28/// The maximum size of a FIDL channel message is 64KB. We use 60KB as a safe limit
29/// to leave headroom for serialization overhead and other message headers.
30pub(crate) const MAX_BATCH_SIZE_BYTES: usize = 60 * 1024;
31
32/// Estimated overhead per packet in a batched FIDL Send/Receive request.
33/// (16 bytes vector header + up to 8 bytes padding).
34pub(crate) const PACKET_OVERHEAD: usize = 24;
35
36/// The Channel mode in use for a L2CAP channel.
37#[derive(PartialEq, Debug, Clone)]
38pub enum ChannelMode {
39    Basic,
40    EnhancedRetransmissionMode,
41    LeCreditBasedFlowControl,
42    EnhancedCreditBasedFlowControl,
43}
44
45impl fmt::Display for ChannelMode {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match self {
48            ChannelMode::Basic => write!(f, "Basic"),
49            ChannelMode::EnhancedRetransmissionMode => write!(f, "ERTM"),
50            ChannelMode::LeCreditBasedFlowControl => write!(f, "LE_Credit"),
51            ChannelMode::EnhancedCreditBasedFlowControl => write!(f, "Credit"),
52        }
53    }
54}
55
56pub enum A2dpDirection {
57    Normal,
58    Source,
59    Sink,
60}
61
62impl From<A2dpDirection> for bredr::A2dpDirectionPriority {
63    fn from(pri: A2dpDirection) -> Self {
64        match pri {
65            A2dpDirection::Normal => bredr::A2dpDirectionPriority::Normal,
66            A2dpDirection::Source => bredr::A2dpDirectionPriority::Source,
67            A2dpDirection::Sink => bredr::A2dpDirectionPriority::Sink,
68        }
69    }
70}
71
72impl TryFrom<fidl_bt::ChannelMode> for ChannelMode {
73    type Error = Error;
74    fn try_from(fidl: fidl_bt::ChannelMode) -> Result<Self, Error> {
75        match fidl {
76            fidl_bt::ChannelMode::Basic => Ok(ChannelMode::Basic),
77            fidl_bt::ChannelMode::EnhancedRetransmission => {
78                Ok(ChannelMode::EnhancedRetransmissionMode)
79            }
80            fidl_bt::ChannelMode::LeCreditBasedFlowControl => {
81                Ok(ChannelMode::LeCreditBasedFlowControl)
82            }
83            fidl_bt::ChannelMode::EnhancedCreditBasedFlowControl => {
84                Ok(ChannelMode::EnhancedCreditBasedFlowControl)
85            }
86            x => Err(Error::FailedConversion(format!("Unsupported channel mode type: {x:?}"))),
87        }
88    }
89}
90
91impl From<ChannelMode> for fidl_bt::ChannelMode {
92    fn from(x: ChannelMode) -> Self {
93        match x {
94            ChannelMode::Basic => fidl_bt::ChannelMode::Basic,
95            ChannelMode::EnhancedRetransmissionMode => fidl_bt::ChannelMode::EnhancedRetransmission,
96            ChannelMode::LeCreditBasedFlowControl => fidl_bt::ChannelMode::LeCreditBasedFlowControl,
97            ChannelMode::EnhancedCreditBasedFlowControl => {
98                fidl_bt::ChannelMode::EnhancedCreditBasedFlowControl
99            }
100        }
101    }
102}
103
104#[derive(PartialEq, Clone, Copy, Debug)]
105pub enum ConnectionBackendType {
106    Socket,
107    FidlClient,
108    FidlServer,
109}
110
111/// A trait representing a Bluetooth data connection.
112/// Concrete implementations handle the specific transport mechanism (e.g., socket or FIDL protocol)
113/// while fulfilling the `Sink` and `Stream` contracts for data transfer.
114pub trait Connection:
115    Stream<Item = Result<Vec<u8>, zx::Status>>
116    + Sink<Vec<u8>, Error = zx::Status>
117    + Send
118    + Sync
119    + std::fmt::Debug
120    + Unpin
121{
122    /// Returns a future that resolves when the connection is closed.
123    fn closed<'a>(&'a self) -> Pin<Box<dyn Future<Output = Result<(), zx::Status>> + 'a>>;
124
125    /// Returns the type of the connection backend.
126    fn connection_type(&self) -> ConnectionBackendType;
127
128    /// Writes data to the connection. This is a non-blocking fast path.
129    /// Returns `SHOULD_WAIT` if the buffer is full.
130    fn write(&self, bytes: &[u8]) -> Result<usize, zx::Status>;
131
132    /// Returns true if the connection is currently closed.
133    fn is_closed(&self) -> bool;
134
135    /// Consumes the connection and returns a partially filled FIDL channel
136    /// containing the transport (e.g., socket handle) if applicable.
137    fn into_fidl_channel(self: Box<Self>) -> Result<bredr::Channel, zx::Status>;
138}
139
140/// A wrapper for Bluetooth channel. Profiles interact with this struct.
141#[derive(Debug)]
142pub struct Channel {
143    pub(crate) connection: Box<dyn Connection>,
144    mode: ChannelMode,
145    max_tx_size: usize,
146    flush_timeout: Arc<Mutex<Option<zx::MonotonicDuration>>>,
147    audio_direction_ext: Option<bredr::AudioDirectionExtProxy>,
148    l2cap_parameters_ext: Option<bredr::L2capParametersExtProxy>,
149    audio_offload_ext: Option<bredr::AudioOffloadExtProxy>,
150    terminated: bool,
151}
152
153impl Channel {
154    pub const DEFAULT_MAX_TX: usize = 672;
155
156    pub fn from_socket(socket: zx::Socket, max_tx_size: usize) -> Result<Self, zx::Status> {
157        let connection = Box::new(SocketConnection::new(socket));
158        Ok(Channel {
159            connection,
160            mode: ChannelMode::Basic,
161            max_tx_size,
162            flush_timeout: Arc::new(Mutex::new(None)),
163            audio_direction_ext: None,
164            l2cap_parameters_ext: None,
165            audio_offload_ext: None,
166            terminated: false,
167        })
168    }
169
170    pub fn from_fidl_client(proxy: fidl_bt::ChannelProxy, max_tx_size: usize) -> Self {
171        let connection = Box::new(FidlClientConnection::new(proxy, max_tx_size));
172        Channel {
173            connection,
174            mode: ChannelMode::Basic,
175            max_tx_size,
176            flush_timeout: Arc::new(Mutex::new(None)),
177            audio_direction_ext: None,
178            l2cap_parameters_ext: None,
179            audio_offload_ext: None,
180            terminated: false,
181        }
182    }
183
184    pub fn from_fidl_server(
185        request_stream: fidl_bt::ChannelRequestStream,
186        max_tx_size: usize,
187    ) -> Self {
188        let connection = Box::new(FidlServerConnection::new(request_stream, max_tx_size));
189        Channel {
190            connection,
191            mode: ChannelMode::Basic,
192            max_tx_size,
193            flush_timeout: Arc::new(Mutex::new(None)),
194            audio_direction_ext: None,
195            l2cap_parameters_ext: None,
196            audio_offload_ext: None,
197            terminated: false,
198        }
199    }
200
201    pub fn from_socket_infallible(socket: zx::Socket, max_tx_size: usize) -> Self {
202        Self::from_socket(socket, max_tx_size).unwrap()
203    }
204
205    pub fn create_socket_pair() -> (Self, Self) {
206        Self::create_socket_pair_with_max_tx(Self::DEFAULT_MAX_TX)
207    }
208
209    pub fn create_socket_pair_with_max_tx(max_tx_size: usize) -> (Self, Self) {
210        let (remote, local) = zx::Socket::create_datagram();
211        (
212            Channel::from_socket(remote, max_tx_size).unwrap(),
213            Channel::from_socket(local, max_tx_size).unwrap(),
214        )
215    }
216
217    pub fn max_tx_size(&self) -> usize {
218        self.max_tx_size
219    }
220
221    pub fn connection_type(&self) -> ConnectionBackendType {
222        self.connection.connection_type()
223    }
224
225    pub fn channel_mode(&self) -> &ChannelMode {
226        &self.mode
227    }
228
229    pub fn flush_timeout(&self) -> Option<zx::MonotonicDuration> {
230        self.flush_timeout.lock().clone()
231    }
232
233    pub fn closed<'a>(&'a self) -> impl Future<Output = Result<(), zx::Status>> + 'a {
234        self.connection.closed()
235    }
236
237    pub fn is_closed(&self) -> bool {
238        self.connection.is_closed()
239    }
240
241    pub fn write(&self, bytes: &[u8]) -> Result<usize, zx::Status> {
242        self.connection.write(bytes)
243    }
244
245    pub fn set_audio_priority(
246        &self,
247        dir: A2dpDirection,
248    ) -> impl Future<Output = Result<(), Error>> + use<> {
249        let proxy = self.audio_direction_ext.clone();
250        async move {
251            match proxy {
252                None => return Err(Error::profile("audio priority not supported")),
253                Some(proxy) => proxy
254                    .set_priority(dir.into())
255                    .await?
256                    .map_err(|e| Error::profile(format!("setting priority failed: {e:?}"))),
257            }
258        }
259    }
260
261    pub fn set_flush_timeout(
262        &self,
263        duration: Option<zx::MonotonicDuration>,
264    ) -> impl Future<Output = Result<Option<zx::MonotonicDuration>, Error>> + use<> {
265        let flush_timeout = self.flush_timeout.clone();
266        let current = self.flush_timeout.lock().clone();
267        let proxy = self.l2cap_parameters_ext.clone();
268        async move {
269            match (current, duration) {
270                (None, None) => return Ok(None),
271                (Some(old), Some(new)) if (old - new).into_millis().abs() < 2 => {
272                    return Ok(current);
273                }
274                _ => {}
275            };
276            let proxy =
277                proxy.ok_or_else(|| Error::profile("l2cap parameter changing not supported"))?;
278            let parameters = fidl_bt::ChannelParameters {
279                flush_timeout: duration.clone().map(zx::MonotonicDuration::into_nanos),
280                ..Default::default()
281            };
282            let new_params = proxy.request_parameters(&parameters).await?;
283            let new_timeout = new_params.flush_timeout.map(zx::MonotonicDuration::from_nanos);
284            *(flush_timeout.lock()) = new_timeout.clone();
285            Ok(new_timeout)
286        }
287    }
288
289    pub fn audio_offload(&self) -> Option<bredr::AudioOffloadExtProxy> {
290        self.audio_offload_ext.clone()
291    }
292}
293
294impl Stream for Channel {
295    type Item = Result<Vec<u8>, zx::Status>;
296
297    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
298        let this = self.get_mut();
299        if this.terminated {
300            warn!("Stream was polled after termination");
301            return Poll::Ready(None);
302        }
303        let res = this.connection.poll_next_unpin(cx);
304        if let Poll::Ready(None) = res {
305            this.terminated = true;
306        }
307        res
308    }
309}
310
311impl FusedStream for Channel {
312    fn is_terminated(&self) -> bool {
313        self.terminated
314    }
315}
316
317impl Sink<Vec<u8>> for Channel {
318    type Error = zx::Status;
319
320    fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
321        Pin::new(&mut *self.get_mut().connection).poll_ready(cx)
322    }
323
324    fn start_send(self: Pin<&mut Self>, item: Vec<u8>) -> Result<(), Self::Error> {
325        Pin::new(&mut *self.get_mut().connection).start_send(item)
326    }
327
328    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
329        Pin::new(&mut *self.get_mut().connection).poll_flush(cx)
330    }
331
332    fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
333        Pin::new(&mut *self.get_mut().connection).poll_close(cx)
334    }
335}
336
337impl TryFrom<Channel> for bredr::Channel {
338    type Error = Error;
339
340    fn try_from(channel: Channel) -> Result<Self, Self::Error> {
341        let mut fidl_channel = channel
342            .connection
343            .into_fidl_channel()
344            .map_err(|e| Error::profile(format!("Failed to convert to FIDL channel: {e:?}")))?;
345
346        fidl_channel.channel_mode = Some(channel.mode.into());
347        fidl_channel.max_tx_sdu_size = Some(channel.max_tx_size as u16);
348
349        let flush_timeout = channel.flush_timeout.lock().clone();
350        fidl_channel.flush_timeout = flush_timeout.map(zx::MonotonicDuration::into_nanos);
351
352        fidl_channel.ext_direction = channel
353            .audio_direction_ext
354            .map(|proxy| {
355                let chan = proxy.into_channel()?;
356                Ok(ClientEnd::new(chan.into()))
357            })
358            .transpose()
359            .map_err(|_: bredr::AudioDirectionExtProxy| {
360                Error::profile("AudioDirection proxy in use")
361            })?;
362
363        fidl_channel.ext_l2cap = channel
364            .l2cap_parameters_ext
365            .map(|proxy| {
366                let chan = proxy.into_channel()?;
367                Ok(ClientEnd::new(chan.into()))
368            })
369            .transpose()
370            .map_err(|_: bredr::L2capParametersExtProxy| {
371                Error::profile("l2cap parameters proxy in use")
372            })?;
373
374        fidl_channel.ext_audio_offload = channel
375            .audio_offload_ext
376            .map(|proxy| {
377                let chan = proxy.into_channel()?;
378                Ok(ClientEnd::new(chan.into()))
379            })
380            .transpose()
381            .map_err(|_: bredr::AudioOffloadExtProxy| {
382                Error::profile("audio offload proxy in use")
383            })?;
384
385        Ok(fidl_channel)
386    }
387}
388
389impl TryFrom<fidl_fuchsia_bluetooth_bredr::Channel> for Channel {
390    type Error = zx::Status;
391
392    fn try_from(fidl: bredr::Channel) -> Result<Self, Self::Error> {
393        let mode = match fidl.channel_mode.unwrap_or(fidl_bt::ChannelMode::Basic).try_into() {
394            Err(e) => {
395                warn!("Unsupported channel mode type: {e:?}");
396                return Err(zx::Status::INTERNAL);
397            }
398            Ok(c) => c,
399        };
400
401        let max_tx_size = fidl.max_tx_sdu_size.ok_or(zx::Status::INVALID_ARGS)? as usize;
402
403        let connection: Box<dyn Connection> = if let Some(conn) = fidl.connection {
404            let proxy = conn.into_proxy();
405            Box::new(FidlClientConnection::new(proxy, max_tx_size)) as Box<dyn Connection>
406        } else if let Some(socket) = fidl.socket {
407            Box::new(SocketConnection::new(socket)) as Box<dyn Connection>
408        } else {
409            return Err(zx::Status::INVALID_ARGS);
410        };
411
412        Ok(Self {
413            connection,
414            mode,
415            max_tx_size,
416            flush_timeout: Arc::new(Mutex::new(
417                fidl.flush_timeout.map(zx::MonotonicDuration::from_nanos),
418            )),
419            audio_direction_ext: fidl.ext_direction.map(|e| e.into_proxy()),
420            l2cap_parameters_ext: fidl.ext_l2cap.map(|e| e.into_proxy()),
421            audio_offload_ext: fidl.ext_audio_offload.map(|c| c.into_proxy()),
422            terminated: false,
423        })
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430    use fidl::endpoints::create_request_stream;
431    use fidl_fuchsia_bluetooth as fidl_bt;
432    use fidl_fuchsia_bluetooth_bredr as bredr;
433    use fuchsia_async as fasync;
434    use futures::StreamExt;
435    use std::pin::pin;
436
437    fn build_socket_bredr_channel() -> (bredr::Channel, zx::Socket) {
438        let (remote, local) = zx::Socket::create_datagram();
439        (
440            bredr::Channel {
441                socket: Some(remote),
442                channel_mode: Some(fidl_bt::ChannelMode::Basic),
443                max_tx_sdu_size: Some(1004),
444                ..Default::default()
445            },
446            local,
447        )
448    }
449
450    #[test]
451    fn direction_ext() {
452        let mut exec = fasync::TestExecutor::new();
453
454        let (no_ext, _local) = build_socket_bredr_channel();
455        let channel = Channel::try_from(no_ext).unwrap();
456
457        assert!(
458            exec.run_singlethreaded(channel.set_audio_priority(A2dpDirection::Normal)).is_err()
459        );
460        assert!(exec.run_singlethreaded(channel.set_audio_priority(A2dpDirection::Sink)).is_err());
461
462        let (mut ext, _local) = build_socket_bredr_channel();
463        let (client_end, mut direction_request_stream) =
464            create_request_stream::<bredr::AudioDirectionExtMarker>();
465        ext.ext_direction = Some(client_end);
466
467        let channel = Channel::try_from(ext).unwrap();
468
469        let audio_direction_fut = channel.set_audio_priority(A2dpDirection::Normal);
470        let mut audio_direction_fut = pin!(audio_direction_fut);
471
472        assert!(exec.run_until_stalled(&mut audio_direction_fut).is_pending());
473
474        match exec.run_until_stalled(&mut direction_request_stream.next()) {
475            Poll::Ready(Some(Ok(bredr::AudioDirectionExtRequest::SetPriority {
476                priority,
477                responder,
478            }))) => {
479                assert_eq!(bredr::A2dpDirectionPriority::Normal, priority);
480                responder.send(Ok(())).expect("response to send cleanly");
481            }
482            x => panic!("Expected a item to be ready on the request stream, got {:?}", x),
483        };
484
485        match exec.run_until_stalled(&mut audio_direction_fut) {
486            Poll::Ready(Ok(())) => {}
487            _x => panic!("Expected ok result from audio direction response"),
488        };
489
490        let audio_direction_fut = channel.set_audio_priority(A2dpDirection::Sink);
491        let mut audio_direction_fut = pin!(audio_direction_fut);
492
493        assert!(exec.run_until_stalled(&mut audio_direction_fut).is_pending());
494
495        match exec.run_until_stalled(&mut direction_request_stream.next()) {
496            Poll::Ready(Some(Ok(bredr::AudioDirectionExtRequest::SetPriority {
497                priority,
498                responder,
499            }))) => {
500                assert_eq!(bredr::A2dpDirectionPriority::Sink, priority);
501                responder
502                    .send(Err(fidl_fuchsia_bluetooth::ErrorCode::Failed))
503                    .expect("response to send cleanly");
504            }
505            x => panic!("Expected a item to be ready on the request stream, got {:?}", x),
506        };
507
508        match exec.run_until_stalled(&mut audio_direction_fut) {
509            Poll::Ready(Err(_)) => {}
510            _x => panic!("Expected error result from audio direction response"),
511        };
512    }
513
514    #[test]
515    fn flush_timeout() {
516        let mut exec = fasync::TestExecutor::new();
517
518        let (mut no_ext, _local) = build_socket_bredr_channel();
519        no_ext.flush_timeout = Some(50_000_000); // 50 milliseconds
520        let channel = Channel::try_from(no_ext).unwrap();
521
522        assert_eq!(Some(zx::MonotonicDuration::from_millis(50)), channel.flush_timeout());
523
524        // Within 2 milliseconds, doesn't change.
525        let res = exec.run_singlethreaded(
526            channel.set_flush_timeout(Some(zx::MonotonicDuration::from_millis(49))),
527        );
528        assert_eq!(Some(zx::MonotonicDuration::from_millis(50)), res.expect("shouldn't error"));
529        let res = exec.run_singlethreaded(
530            channel.set_flush_timeout(Some(zx::MonotonicDuration::from_millis(51))),
531        );
532        assert_eq!(Some(zx::MonotonicDuration::from_millis(50)), res.expect("shouldn't error"));
533
534        assert!(
535            exec.run_singlethreaded(
536                channel.set_flush_timeout(Some(zx::MonotonicDuration::from_millis(200)))
537            )
538            .is_err()
539        );
540        assert!(exec.run_singlethreaded(channel.set_flush_timeout(None)).is_err());
541
542        let (mut ext, _local) = build_socket_bredr_channel();
543        let (client_end, mut l2cap_request_stream) =
544            create_request_stream::<bredr::L2capParametersExtMarker>();
545        ext.ext_l2cap = Some(client_end);
546
547        let channel = Channel::try_from(ext).unwrap();
548
549        {
550            let flush_timeout_fut = channel.set_flush_timeout(None);
551            let mut flush_timeout_fut = pin!(flush_timeout_fut);
552
553            // Requesting no change returns right away with no change.
554            match exec.run_until_stalled(&mut flush_timeout_fut) {
555                Poll::Ready(Ok(None)) => {}
556                x => panic!("Expected no flush timeout to not stall, got {:?}", x),
557            }
558        }
559
560        let req_duration = zx::MonotonicDuration::from_millis(42);
561
562        {
563            let flush_timeout_fut = channel.set_flush_timeout(Some(req_duration));
564            let mut flush_timeout_fut = pin!(flush_timeout_fut);
565
566            assert!(exec.run_until_stalled(&mut flush_timeout_fut).is_pending());
567
568            match exec.run_until_stalled(&mut l2cap_request_stream.next()) {
569                Poll::Ready(Some(Ok(bredr::L2capParametersExtRequest::RequestParameters {
570                    request,
571                    responder,
572                }))) => {
573                    assert_eq!(Some(req_duration.into_nanos()), request.flush_timeout);
574                    // Send a different response
575                    let params = fidl_bt::ChannelParameters {
576                        flush_timeout: Some(50_000_000), // 50ms
577                        ..Default::default()
578                    };
579                    responder.send(&params).expect("response to send cleanly");
580                }
581                x => panic!("Expected a item to be ready on the request stream, got {:?}", x),
582            };
583
584            match exec.run_until_stalled(&mut flush_timeout_fut) {
585                Poll::Ready(Ok(Some(duration))) => {
586                    assert_eq!(zx::MonotonicDuration::from_millis(50), duration)
587                }
588                x => panic!("Expected ready result from params response, got {:?}", x),
589            };
590        }
591
592        // Channel should have recorded the new flush timeout.
593        assert_eq!(Some(zx::MonotonicDuration::from_millis(50)), channel.flush_timeout());
594    }
595
596    #[test]
597    fn audio_offload() {
598        let _exec = fasync::TestExecutor::new();
599
600        let (no_ext, _local) = build_socket_bredr_channel();
601        let channel = Channel::try_from(no_ext).unwrap();
602
603        assert!(channel.audio_offload().is_none());
604
605        let (mut ext, _local) = build_socket_bredr_channel();
606        let (client_end, mut _audio_offload_ext_req_stream) =
607            create_request_stream::<bredr::AudioOffloadExtMarker>();
608        ext.ext_audio_offload = Some(client_end);
609
610        let channel = Channel::try_from(ext).unwrap();
611
612        let offload_ext = channel.audio_offload();
613        assert!(offload_ext.is_some());
614        // We can get the audio offload multiple times without dropping
615        assert!(channel.audio_offload().is_some());
616        // And with dropping
617        drop(offload_ext);
618        assert!(channel.audio_offload().is_some());
619    }
620
621    #[test]
622    fn channel_from_fidl_priority() {
623        let _exec = fasync::TestExecutor::new();
624
625        // Case 1: Both FIDL connection and socket are present.
626        // FIDL connection should be preferred over socket.
627        let (client_end, _server_end) =
628            fidl::endpoints::create_endpoints::<fidl_bt::ChannelMarker>();
629        let (mut fidl_both, _socket_local) = build_socket_bredr_channel();
630        fidl_both.connection = Some(client_end);
631
632        let chan = Channel::try_from(fidl_both).expect("to convert successfully");
633        assert_eq!(chan.connection.connection_type(), ConnectionBackendType::FidlClient);
634
635        // Case 2: Only socket is present.
636        // Should fall back to traditional socket transport.
637        let (socket_only, _socket_local) = build_socket_bredr_channel();
638
639        let chan = Channel::try_from(socket_only).expect("to convert successfully");
640        assert_eq!(chan.connection.connection_type(), ConnectionBackendType::Socket);
641
642        // Case 3: Neither is present.
643        // Should fail to convert as we need at least one transport.
644        let fidl_empty = bredr::Channel {
645            channel_mode: Some(fidl_bt::ChannelMode::Basic),
646            max_tx_sdu_size: Some(1004),
647            ..Default::default()
648        };
649        assert!(Channel::try_from(fidl_empty).is_err());
650    }
651}