blob: 5abb3fa63ac3f800320528f37db9cea48fab45b8 [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);
/// All 13 mandatory characteristics defined in MCS v1.0.1 Section 3.
fn mandatory_characteristics() -> [Characteristic; 13] {
[
build_mandatory_characteristic(
MEDIA_PLAYER_NAME_HANDLE,
MEDIA_PLAYER_NAME_UUID,
CharacteristicProperties::READ_NOTIFY,
),
build_mandatory_characteristic(
TRACK_CHANGED_HANDLE,
TRACK_CHANGED_UUID,
CharacteristicProperty::Notify,
),
build_mandatory_characteristic(
TRACK_TITLE_HANDLE,
TRACK_TITLE_UUID,
CharacteristicProperties::READ_NOTIFY,
),
build_mandatory_characteristic(
TRACK_DURATION_HANDLE,
TRACK_DURATION_UUID,
CharacteristicProperties::READ_NOTIFY,
),
build_mandatory_characteristic(
TRACK_POSITION_HANDLE,
TRACK_POSITION_UUID,
CharacteristicProperties::READ_WRITE_NOTIFY,
),
build_mandatory_characteristic(
PLAYBACK_SPEED_HANDLE,
PLAYBACK_SPEED_UUID,
CharacteristicProperties::READ_WRITE_NOTIFY,
),
build_mandatory_characteristic(
SEEKING_SPEED_HANDLE,
SEEKING_SPEED_UUID,
CharacteristicProperties::READ_NOTIFY,
),
build_mandatory_characteristic(
PLAYING_ORDER_HANDLE,
PLAYING_ORDER_UUID,
CharacteristicProperties::READ_WRITE_NOTIFY,
),
build_mandatory_characteristic(
PLAYING_ORDERS_SUPPORTED_HANDLE,
PLAYING_ORDERS_SUPPORTED_UUID,
CharacteristicProperty::Read,
),
build_mandatory_characteristic(
MEDIA_STATE_HANDLE,
MEDIA_STATE_UUID,
CharacteristicProperties::READ_NOTIFY,
),
build_mandatory_characteristic(
MEDIA_CONTROL_POINT_HANDLE,
MEDIA_CONTROL_POINT_UUID,
CharacteristicProperties::WRITE_NOTIFY,
),
build_mandatory_characteristic(
MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_HANDLE,
MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_UUID,
CharacteristicProperties::READ_NOTIFY,
),
build_mandatory_characteristic(
CONTENT_CONTROL_ID_HANDLE,
CONTENT_CONTROL_ID_UUID,
CharacteristicProperty::Read,
),
]
}
/// Specification of a mandatory GATT characteristic for MCS.
/// 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(
handle: Handle,
uuid: Uuid,
properties: impl Into<CharacteristicProperties>,
) -> Characteristic {
let properties = properties.into();
Characteristic {
handle,
uuid,
properties,
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 chrc in mandatory_characteristics() {
service_def.add_characteristic(chrc)?;
}
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();
let expected_mandatory = mandatory_characteristics();
assert_eq!(characteristics.len(), expected_mandatory.len());
for (i, expected) in expected_mandatory.iter().enumerate() {
let chrc = characteristics[i];
assert_eq!(chrc.handle, expected.handle);
assert_eq!(chrc.uuid, expected.uuid);
assert_eq!(chrc.properties, expected.properties);
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.
}