blob: f4b43d0c7a3aae3dad5af4d74297ace4ecc861fb [file]
// 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.
}