fuchsia_bluetooth/profile/
avrcp.rs

1// Copyright 2025 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 crate::profile::{Psm, elem_to_profile_descriptor, psm_from_protocol};
6use crate::types::Uuid;
7use anyhow::{Error, format_err};
8use bitflags::bitflags;
9use fidl_fuchsia_bluetooth_bredr::*;
10use log::info;
11
12bitflags! {
13    /// Represents the features supported by an AVRCP Target.
14    /// Defined in AVRCP v1.6.3, Table 8.2.
15    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
16    pub struct AvrcpTargetFeatures: u16 {
17        const CATEGORY1         = 1 << 0;
18        const CATEGORY2         = 1 << 1;
19        const CATEGORY3         = 1 << 2;
20        const CATEGORY4         = 1 << 3;
21        const PLAYERSETTINGS    = 1 << 4;
22        const GROUPNAVIGATION   = 1 << 5;
23        const SUPPORTSBROWSING  = 1 << 6;
24        const SUPPORTSMULTIPLEMEDIAPLAYERS = 1 << 7;
25        const SUPPORTSCOVERART  = 1 << 8;
26        // 9-15 Reserved
27    }
28}
29
30bitflags! {
31    /// Represents the features supported by an AVRCP Controller.
32    /// Defined in AVRCP v1.6.3, Table 8.1.
33    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
34    pub struct AvrcpControllerFeatures: u16 {
35        const CATEGORY1         = 1 << 0;
36        const CATEGORY2         = 1 << 1;
37        const CATEGORY3         = 1 << 2;
38        const CATEGORY4         = 1 << 3;
39        // 4-5 RESERVED
40        const SUPPORTSBROWSING  = 1 << 6;
41        const SUPPORTSCOVERARTGETIMAGEPROPERTIES = 1 << 7;
42        const SUPPORTSCOVERARTGETIMAGE = 1 << 8;
43        const SUPPORTSCOVERARTGETLINKEDTHUMBNAIL = 1 << 9;
44        // 10-15 RESERVED
45    }
46}
47
48impl AvrcpControllerFeatures {
49    /// Returns true if the controller supports any of the cover art features.
50    pub fn supports_cover_art(&self) -> bool {
51        self.contains(
52            AvrcpControllerFeatures::SUPPORTSCOVERARTGETIMAGE
53                | AvrcpControllerFeatures::SUPPORTSCOVERARTGETIMAGEPROPERTIES
54                | AvrcpControllerFeatures::SUPPORTSCOVERARTGETLINKEDTHUMBNAIL,
55        )
56    }
57}
58
59pub const SDP_SUPPORTED_FEATURES: u16 = 0x0311;
60
61pub const AV_REMOTE_TARGET_CLASS: u16 = 0x110c;
62pub const AV_REMOTE_CLASS: u16 = 0x110e;
63pub const AV_REMOTE_CONTROLLER_CLASS: u16 = 0x110f;
64
65/// Represents the AVRCP protocol version.
66#[derive(PartialEq, Hash, Clone, Copy)]
67pub struct AvrcpProtocolVersion(pub u8, pub u8);
68
69impl std::fmt::Debug for AvrcpProtocolVersion {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        write!(f, "{}.{}", self.0, self.1)
72    }
73}
74
75/// Represents a discovered AVRCP service on a remote peer.
76#[derive(Debug, PartialEq, Clone, Copy)]
77pub enum AvrcpService {
78    Target {
79        features: AvrcpTargetFeatures,
80        psm: Psm,
81        protocol_version: AvrcpProtocolVersion,
82    },
83    Controller {
84        features: AvrcpControllerFeatures,
85        psm: Psm,
86        protocol_version: AvrcpProtocolVersion,
87    },
88}
89
90impl AvrcpService {
91    /// Returns true if the service supports browsing.
92    pub fn supports_browsing(&self) -> bool {
93        match &self {
94            Self::Target { features, .. } => {
95                features.contains(AvrcpTargetFeatures::SUPPORTSBROWSING)
96            }
97            Self::Controller { features, .. } => {
98                features.contains(AvrcpControllerFeatures::SUPPORTSBROWSING)
99            }
100        }
101    }
102
103    /// Returns true if the service supports absolute volume.
104    ///
105    /// Per AVRCP v1.6.3, Table 3.1, Condition C4, support for Absolute Volume is mandatory
106    /// if Category 2 is supported.
107    pub fn supports_absolute_volume(&self) -> bool {
108        match &self {
109            Self::Target { features, .. } => features.contains(AvrcpTargetFeatures::CATEGORY2),
110            Self::Controller { features, .. } => {
111                features.contains(AvrcpControllerFeatures::CATEGORY2)
112            }
113        }
114    }
115
116    /// Attempts to parse an `AvrcpService` from a `bredr.ServiceSearchResult`.
117    ///
118    /// Returns an `Error` if the provided search result is missing any of the required
119    /// AVRCP attributes.
120    pub fn from_search_result(
121        protocol: Vec<ProtocolDescriptor>,
122        attributes: Vec<Attribute>,
123    ) -> Result<AvrcpService, Error> {
124        let mut features: Option<u16> = None;
125        let mut service_uuids: Option<Vec<Uuid>> = None;
126        let mut profile: Option<ProfileDescriptor> = None;
127
128        // Both the `protocol` and `attributes` should contain the primary protocol descriptor. It
129        // is simpler to parse the former.
130        let protocol = protocol
131            .iter()
132            .map(|proto| crate::profile::ProtocolDescriptor::try_from(proto))
133            .collect::<Result<Vec<_>, _>>()?;
134        let psm = psm_from_protocol(&protocol)
135            .ok_or_else(|| format_err!("AVRCP Service with no L2CAP PSM"))?;
136
137        for attr in attributes {
138            match attr.id {
139                Some(ATTR_SERVICE_CLASS_ID_LIST) => {
140                    if let Some(DataElement::Sequence(seq)) = attr.element {
141                        let uuids: Vec<Uuid> = seq
142                            .into_iter()
143                            .flatten()
144                            .filter_map(|item| match *item {
145                                DataElement::Uuid(uuid) => Some(uuid.into()),
146                                _ => None,
147                            })
148                            .collect();
149                        if !uuids.is_empty() {
150                            service_uuids = Some(uuids);
151                        }
152                    }
153                }
154                Some(ATTR_BLUETOOTH_PROFILE_DESCRIPTOR_LIST) => {
155                    if let Some(DataElement::Sequence(profiles)) = attr.element {
156                        for elem in profiles {
157                            let elem = elem.expect("DataElement sequence elements should exist");
158                            profile = elem_to_profile_descriptor(&*elem);
159                        }
160                    }
161                }
162                Some(SDP_SUPPORTED_FEATURES) => {
163                    if let Some(DataElement::Uint16(value)) = attr.element {
164                        features = Some(value);
165                    }
166                }
167                _ => {}
168            }
169        }
170
171        let (service_uuids, features, profile) = match (service_uuids, features, profile) {
172            (Some(s), Some(f), Some(p)) => (s, f, p),
173            (s, f, p) => {
174                let err = format_err!(
175                    "{}{}{}missing in service attrs",
176                    if s.is_some() { "" } else { "Class UUIDs " },
177                    if f.is_some() { "" } else { "Features " },
178                    if p.is_some() { "" } else { "Profile " }
179                );
180                return Err(err);
181            }
182        };
183
184        // The L2CAP PSM should always be PSM_AVCTP. However, in unexpected cases, the peer may try
185        // to advertise a different PSM for its AVRCP service.
186        if psm != Psm::AVCTP {
187            info!("Found AVRCP Service with non standard PSM: {:?}", psm);
188        }
189
190        let (Some(major_version), Some(minor_version)) =
191            (profile.major_version, profile.minor_version)
192        else {
193            return Err(format_err!("ProfileDescriptor missing minor/major version"));
194        };
195        let protocol_version = AvrcpProtocolVersion(major_version, minor_version);
196
197        if service_uuids.contains(&Uuid::new16(AV_REMOTE_TARGET_CLASS)) {
198            let features = AvrcpTargetFeatures::from_bits_truncate(features);
199            return Ok(AvrcpService::Target { features, psm, protocol_version });
200        } else if service_uuids.contains(&Uuid::new16(AV_REMOTE_CLASS))
201            || service_uuids.contains(&Uuid::new16(AV_REMOTE_CONTROLLER_CLASS))
202        {
203            let features = AvrcpControllerFeatures::from_bits_truncate(features);
204            return Ok(AvrcpService::Controller { features, psm, protocol_version });
205        }
206        Err(format_err!("Failed to find any applicable services for AVRCP"))
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use assert_matches::assert_matches;
214
215    fn build_attributes(
216        service_class: bool,
217        profile_descriptor: bool,
218        sdp_features: bool,
219    ) -> Vec<Attribute> {
220        let mut attrs = Vec::new();
221        if service_class {
222            attrs.push(Attribute {
223                id: Some(ATTR_SERVICE_CLASS_ID_LIST),
224                element: Some(DataElement::Sequence(vec![Some(Box::new(DataElement::Uuid(
225                    Uuid::new16(AV_REMOTE_TARGET_CLASS).into(),
226                )))])),
227                ..Default::default()
228            });
229        }
230        if profile_descriptor {
231            attrs.push(Attribute {
232                id: Some(ATTR_BLUETOOTH_PROFILE_DESCRIPTOR_LIST),
233                element: Some(DataElement::Sequence(vec![Some(Box::new(DataElement::Sequence(
234                    vec![
235                        Some(Box::new(DataElement::Uuid(Uuid::new16(4366).into()))),
236                        Some(Box::new(DataElement::Uint16(0xffff))),
237                    ],
238                )))])),
239                ..Default::default()
240            });
241        }
242
243        if sdp_features {
244            attrs.push(Attribute {
245                id: Some(SDP_SUPPORTED_FEATURES), // SDP Attribute "SUPPORTED FEATURES"
246                element: Some(DataElement::Uint16(0xffff)),
247                ..Default::default()
248            });
249        }
250        attrs
251    }
252
253    #[fuchsia::test]
254    fn service_from_search_result() {
255        let attributes = build_attributes(true, true, true);
256        let protocol = vec![ProtocolDescriptor {
257            protocol: Some(ProtocolIdentifier::L2Cap),
258            params: Some(vec![DataElement::Uint16(20)]), // Random PSM is still OK.
259            ..Default::default()
260        }];
261        let service = AvrcpService::from_search_result(protocol, attributes);
262        assert_matches!(service, Ok(_));
263    }
264
265    #[fuchsia::test]
266    fn service_with_missing_features_returns_none() {
267        let no_service_class = build_attributes(false, true, true);
268        let protocol = vec![ProtocolDescriptor {
269            protocol: Some(ProtocolIdentifier::L2Cap),
270            params: Some(vec![DataElement::Uint16(20)]), // Random PSM is still OK.
271            ..Default::default()
272        }];
273        let service = AvrcpService::from_search_result(protocol, no_service_class);
274        assert_matches!(service, Err(_));
275
276        let no_profile_descriptor = build_attributes(true, false, true);
277        let protocol = vec![ProtocolDescriptor {
278            protocol: Some(ProtocolIdentifier::L2Cap),
279            params: Some(vec![DataElement::Uint16(20)]), // Random PSM is still OK.
280            ..Default::default()
281        }];
282        let service = AvrcpService::from_search_result(protocol, no_profile_descriptor);
283        assert_matches!(service, Err(_));
284
285        let no_sdp_features = build_attributes(true, true, false);
286        let protocol = vec![ProtocolDescriptor {
287            protocol: Some(ProtocolIdentifier::L2Cap),
288            params: Some(vec![DataElement::Uint16(20)]), // Random PSM is still OK.
289            ..Default::default()
290        }];
291        let service = AvrcpService::from_search_result(protocol, no_sdp_features);
292        assert_matches!(service, Err(_));
293    }
294
295    #[test]
296    fn service_with_missing_protocol_returns_none() {
297        let attributes = build_attributes(true, true, true);
298        let protocol = vec![];
299        let service = AvrcpService::from_search_result(protocol, attributes);
300        assert_matches!(service, Err(_));
301    }
302}