| // Copyright 2026 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //! Implements the Media Control Service (MCS) server. |
| |
| use bt_common::Uuid; |
| use bt_gatt::server::{ServiceDefinition, ServiceId}; |
| use bt_gatt::types::{ |
| AttributePermissions, CharacteristicProperties, CharacteristicProperty, Handle, SecurityLevels, |
| ServiceKind, |
| }; |
| use bt_gatt::Characteristic; |
| |
| use crate::types::*; |
| use crate::Error; |
| |
| // ============================================================================ |
| // Mandatory Characteristic Handle Definitions (MCS v1.0.1 Section 3) |
| // ============================================================================ |
| |
| /// Handle assigned to the Media Player Name characteristic. |
| const MEDIA_PLAYER_NAME_HANDLE: Handle = Handle(1); |
| /// Handle assigned to the Track Changed characteristic. |
| const TRACK_CHANGED_HANDLE: Handle = Handle(2); |
| /// Handle assigned to the Track Title characteristic. |
| const TRACK_TITLE_HANDLE: Handle = Handle(3); |
| /// Handle assigned to the Track Duration characteristic. |
| const TRACK_DURATION_HANDLE: Handle = Handle(4); |
| /// Handle assigned to the Track Position characteristic. |
| const TRACK_POSITION_HANDLE: Handle = Handle(5); |
| /// Handle assigned to the Playback Speed characteristic. |
| const PLAYBACK_SPEED_HANDLE: Handle = Handle(6); |
| /// Handle assigned to the Seeking Speed characteristic. |
| const SEEKING_SPEED_HANDLE: Handle = Handle(7); |
| /// Handle assigned to the Playing Order characteristic. |
| const PLAYING_ORDER_HANDLE: Handle = Handle(8); |
| /// Handle assigned to the Playing Orders Supported characteristic. |
| const PLAYING_ORDERS_SUPPORTED_HANDLE: Handle = Handle(9); |
| /// Handle assigned to the Media State characteristic. |
| const MEDIA_STATE_HANDLE: Handle = Handle(10); |
| /// Handle assigned to the Media Control Point characteristic. |
| const MEDIA_CONTROL_POINT_HANDLE: Handle = Handle(11); |
| /// Handle assigned to the Media Control Point Opcodes Supported characteristic. |
| const MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_HANDLE: Handle = Handle(12); |
| /// Handle assigned to the Content Control ID (CCID) characteristic. |
| const CONTENT_CONTROL_ID_HANDLE: Handle = Handle(13); |
| |
| /// Specification of a mandatory GATT characteristic for MCS. |
| struct CharacteristicSpec { |
| handle: Handle, |
| uuid: Uuid, |
| properties: &'static [CharacteristicProperty], |
| } |
| |
| /// All the mandatory characteristics defined in MCS v1.0.1 Section 3. |
| const MANDATORY_CHARACTERISTICS: [CharacteristicSpec; 13] = [ |
| CharacteristicSpec { |
| handle: MEDIA_PLAYER_NAME_HANDLE, |
| uuid: MEDIA_PLAYER_NAME_UUID, |
| properties: CharacteristicProperty::READ_NOTIFY, |
| }, |
| CharacteristicSpec { |
| handle: TRACK_CHANGED_HANDLE, |
| uuid: TRACK_CHANGED_UUID, |
| properties: &[CharacteristicProperty::Notify], |
| }, |
| CharacteristicSpec { |
| handle: TRACK_TITLE_HANDLE, |
| uuid: TRACK_TITLE_UUID, |
| properties: CharacteristicProperty::READ_NOTIFY, |
| }, |
| CharacteristicSpec { |
| handle: TRACK_DURATION_HANDLE, |
| uuid: TRACK_DURATION_UUID, |
| properties: CharacteristicProperty::READ_NOTIFY, |
| }, |
| CharacteristicSpec { |
| handle: TRACK_POSITION_HANDLE, |
| uuid: TRACK_POSITION_UUID, |
| properties: CharacteristicProperty::READ_WRITE_NOTIFY, |
| }, |
| CharacteristicSpec { |
| handle: PLAYBACK_SPEED_HANDLE, |
| uuid: PLAYBACK_SPEED_UUID, |
| properties: CharacteristicProperty::READ_WRITE_NOTIFY, |
| }, |
| CharacteristicSpec { |
| handle: SEEKING_SPEED_HANDLE, |
| uuid: SEEKING_SPEED_UUID, |
| properties: CharacteristicProperty::READ_NOTIFY, |
| }, |
| CharacteristicSpec { |
| handle: PLAYING_ORDER_HANDLE, |
| uuid: PLAYING_ORDER_UUID, |
| properties: CharacteristicProperty::READ_WRITE_NOTIFY, |
| }, |
| CharacteristicSpec { |
| handle: PLAYING_ORDERS_SUPPORTED_HANDLE, |
| uuid: PLAYING_ORDERS_SUPPORTED_UUID, |
| properties: &[CharacteristicProperty::Read], |
| }, |
| CharacteristicSpec { |
| handle: MEDIA_STATE_HANDLE, |
| uuid: MEDIA_STATE_UUID, |
| properties: CharacteristicProperty::READ_NOTIFY, |
| }, |
| CharacteristicSpec { |
| handle: MEDIA_CONTROL_POINT_HANDLE, |
| uuid: MEDIA_CONTROL_POINT_UUID, |
| properties: CharacteristicProperty::WRITE_NOTIFY, |
| }, |
| CharacteristicSpec { |
| handle: MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_HANDLE, |
| uuid: MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_UUID, |
| properties: CharacteristicProperty::READ_NOTIFY, |
| }, |
| CharacteristicSpec { |
| handle: CONTENT_CONTROL_ID_HANDLE, |
| uuid: CONTENT_CONTROL_ID_UUID, |
| properties: &[CharacteristicProperty::Read], |
| }, |
| ]; |
| |
| /// Constructs a characteristic definition with the specified handle, UUID, |
| /// properties, and encryption-required permissions conforming to MCS v1.0.1 |
| /// Section 3. |
| fn build_mandatory_characteristic(spec: &CharacteristicSpec) -> Characteristic { |
| let properties: CharacteristicProperties = spec.properties.iter().copied().collect(); |
| Characteristic { |
| handle: spec.handle, |
| uuid: spec.uuid, |
| properties: properties.clone(), |
| permissions: AttributePermissions::with_levels( |
| &properties, |
| &SecurityLevels::encryption_required(), |
| ), |
| descriptors: Vec::new(), |
| } |
| } |
| |
| /// Builder for configuring an MCS or GMCS GATT service. |
| #[derive(Debug, Clone, PartialEq)] |
| pub struct McsServerBuilder { |
| /// Service UUID assigned to the service. |
| service_uuid: Uuid, |
| /// Content Control ID (CCID) identifying this media service instance. This |
| /// is unique across all MCS/GMCS instances on the host server. |
| ccid: u8, |
| /// Human-readable media player application name. |
| player_name: String, |
| } |
| |
| impl McsServerBuilder { |
| /// Creates a builder for the Generic Media Control Service. |
| pub fn generic(ccid: u8, player_name: impl Into<String>) -> Self { |
| Self::new(GENERIC_MEDIA_CONTROL_SERVICE_UUID, ccid, player_name) |
| } |
| |
| /// Creates a builder for an application-specific Media Control Service. |
| pub fn instance(ccid: u8, player_name: impl Into<String>) -> Self { |
| Self::new(MEDIA_CONTROL_SERVICE_UUID, ccid, player_name) |
| } |
| |
| fn new(service_uuid: Uuid, ccid: u8, player_name: impl Into<String>) -> Self { |
| Self { service_uuid, ccid, player_name: player_name.into() } |
| } |
| |
| /// Constructs the complete GATT [`ServiceDefinition`] containing all 13 |
| /// Mandatory Characteristics defined in MCS v1.0.1 Section 3. |
| pub fn build_service_definition(&self) -> Result<ServiceDefinition, Error> { |
| // The local `ServiceId` is derived from the provided `CCID`. This is valid |
| // because the CCID must be unique across all MCS/GMCS instances on the |
| // host server. Adding a duplicate characteristic will result in an |
| // Error. |
| let mut service_def = ServiceDefinition::new( |
| ServiceId::new(self.ccid.into()), |
| self.service_uuid, |
| ServiceKind::Primary, |
| ); |
| |
| for spec in &MANDATORY_CHARACTERISTICS { |
| service_def.add_characteristic(build_mandatory_characteristic(spec))?; |
| } |
| |
| Ok(service_def) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| fn builder_generic_service_definition() { |
| let builder = McsServerBuilder::generic(0x42, "Test Generic Player"); |
| let service_def = builder |
| .build_service_definition() |
| .expect("generic service definition builds successfully"); |
| assert_eq!(service_def.uuid(), GENERIC_MEDIA_CONTROL_SERVICE_UUID); |
| assert_eq!(service_def.id(), ServiceId::new(0x42)); |
| assert_eq!(service_def.kind(), ServiceKind::Primary); |
| assert_eq!(service_def.characteristics().count(), 13); |
| } |
| |
| #[test] |
| fn builder_instance_service_definition() { |
| let builder = McsServerBuilder::instance(0x07, "Test Instance Player"); |
| let service_def = builder |
| .build_service_definition() |
| .expect("instance service definition builds successfully"); |
| assert_eq!(service_def.uuid(), MEDIA_CONTROL_SERVICE_UUID); |
| assert_eq!(service_def.id(), ServiceId::new(0x07)); |
| assert_eq!(service_def.kind(), ServiceKind::Primary); |
| assert_eq!(service_def.characteristics().count(), 13); |
| } |
| |
| #[test] |
| fn mandatory_characteristic_handles_uuids_and_properties() { |
| let builder = McsServerBuilder::generic(0x01, "Player"); |
| let service_def = |
| builder.build_service_definition().expect("service definition builds successfully"); |
| let characteristics: Vec<&Characteristic> = service_def.characteristics().collect(); |
| |
| assert_eq!(characteristics.len(), MANDATORY_CHARACTERISTICS.len()); |
| |
| for (i, spec) in MANDATORY_CHARACTERISTICS.iter().enumerate() { |
| let chrc = characteristics[i]; |
| assert_eq!(chrc.handle, spec.handle); |
| assert_eq!(chrc.uuid, spec.uuid); |
| assert_eq!(chrc.properties.0.as_slice(), spec.properties); |
| |
| let expected_properties: CharacteristicProperties = |
| spec.properties.iter().copied().collect(); |
| let expected_permissions = AttributePermissions::with_levels( |
| &expected_properties, |
| &SecurityLevels::encryption_required(), |
| ); |
| assert_eq!( |
| chrc.permissions.read.map(|s| s.encryption), |
| expected_permissions.read.map(|s| s.encryption) |
| ); |
| assert_eq!( |
| chrc.permissions.write.map(|s| s.encryption), |
| expected_permissions.write.map(|s| s.encryption) |
| ); |
| assert_eq!( |
| chrc.permissions.update.map(|s| s.encryption), |
| expected_permissions.update.map(|s| s.encryption) |
| ); |
| } |
| } |
| |
| #[test] |
| fn service_id_tracks_ccid() { |
| let builder1 = McsServerBuilder::instance(0x05, "Player 1"); |
| let builder2 = McsServerBuilder::instance(0x05, "Player 2"); |
| let builder3 = McsServerBuilder::instance(0x06, "Player 3"); |
| |
| let def1 = builder1.build_service_definition().expect("valid definition"); |
| let def2 = builder2.build_service_definition().expect("valid definition"); |
| let def3 = builder3.build_service_definition().expect("valid definition"); |
| |
| assert_eq!(def1.id(), def2.id()); |
| assert_eq!(def1.id(), ServiceId::new(5)); |
| assert_ne!(def1.id(), def3.id()); |
| assert_eq!(def3.id(), ServiceId::new(6)); |
| } |
| |
| // TODO(b/540400364): Add a test verifying that publishing two McsServer |
| // instances with the same CCID to a GATT server fails with an |
| // AlreadyPublished error. |
| } |