blob: 59e5d68bc8c2937dc64bf870dd2db869d625fb55 [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::{LocalService, Server as _, ServiceDefinition, ServiceEvent, ServiceId};
use bt_gatt::types::{
AttributePermissions, CharacteristicProperties, CharacteristicProperty, Handle, SecurityLevels,
ServiceKind,
};
use bt_gatt::Characteristic;
use futures::stream::Stream;
use pin_project::pin_project;
use std::future::Future;
use std::task::{Context, Poll, Waker};
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(),
}
}
/// Internal state for the MCS server.
#[pin_project(project = LocalServiceProj)]
enum LocalServiceState<T: bt_gatt::ServerTypes> {
/// Service definition has not been registered in the GATT database.
NotPublished {
waker: Option<Waker>,
},
/// Service registration is in progress.
Preparing {
#[pin]
fut: T::LocalServiceFut,
},
/// Service registration is complete and active in the GATT database.
Published {
service: T::LocalService,
#[pin]
events: T::ServiceEventStream,
},
Terminated,
}
impl<T: bt_gatt::ServerTypes> Default for LocalServiceState<T> {
fn default() -> Self {
Self::NotPublished { waker: None }
}
}
impl<T: bt_gatt::ServerTypes> LocalServiceState<T> {
fn is_published(&self) -> bool {
matches!(self, LocalServiceState::Published { .. })
}
}
impl<T: bt_gatt::ServerTypes> Stream for LocalServiceState<T> {
type Item = Result<ServiceEvent<T>, Error>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
loop {
match self.as_mut().project() {
LocalServiceProj::Terminated => return Poll::Ready(None),
LocalServiceProj::NotPublished { waker } => {
*waker = Some(cx.waker().clone());
return Poll::Pending;
}
LocalServiceProj::Preparing { fut } => match futures::ready!(fut.poll(cx)) {
Ok(service) => {
let events = service.publish();
self.as_mut().set(LocalServiceState::Published { service, events });
}
Err(e) => {
self.as_mut().set(LocalServiceState::NotPublished { waker: None });
return Poll::Ready(Some(Err(Error::Gatt(e))));
}
},
LocalServiceProj::Published { service: _, events } => {
match futures::ready!(events.poll_next(cx)) {
Some(Ok(event)) => return Poll::Ready(Some(Ok(event))),
Some(Err(e)) => {
self.as_mut().set(LocalServiceState::Terminated);
return Poll::Ready(Some(Err(Error::Gatt(e))));
}
None => {
self.as_mut().set(LocalServiceState::Terminated);
return Poll::Ready(None);
}
}
}
}
}
}
}
/// 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)
}
/// Builds an [`McsServer`] configured with this builder.
pub fn build<T: bt_gatt::ServerTypes>(self) -> Result<McsServer<T>, Error> {
let service_def = self.build_service_definition()?;
Ok(McsServer {
service_def,
local_service: Default::default(),
ccid: self.ccid,
player_name: self.player_name,
})
}
}
/// An instance of a Media Control Service (MCS) or Generic Media Control
/// Service (GMCS) GATT server.
#[pin_project]
pub struct McsServer<T: bt_gatt::ServerTypes> {
service_def: ServiceDefinition,
#[pin]
local_service: LocalServiceState<T>,
ccid: u8,
player_name: String,
}
impl<T: bt_gatt::ServerTypes> McsServer<T> {
/// Returns true if this server is a GMCS server.
pub fn is_generic_service(&self) -> bool {
self.service_def.uuid() == GENERIC_MEDIA_CONTROL_SERVICE_UUID
}
/// Returns true if the server has successfully published the GATT service.
pub fn is_published(&self) -> bool {
self.local_service.is_published()
}
/// Publishes the service to the GATT database.
pub fn publish(&mut self, server: T::Server) -> Result<(), Error> {
let LocalServiceState::NotPublished { waker } = &mut self.local_service else {
return Err(Error::AlreadyPublished);
};
let waker = waker.take();
self.local_service =
LocalServiceState::Preparing { fut: server.prepare(self.service_def.clone()) };
if let Some(w) = waker {
w.wake();
}
Ok(())
}
}
impl<T: bt_gatt::ServerTypes> Stream for McsServer<T> {
type Item = Result<(), Error>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
loop {
let mut this = self.as_mut().project();
let gatt_event = match futures::ready!(this.local_service.as_mut().poll_next(cx)) {
None => return Poll::Ready(None),
Some(Err(e)) => return Poll::Ready(Some(Err(e))),
Some(Ok(event)) => event,
};
match gatt_event {
// TODO(b/540400364): Add support for characteristic reads and writes
_ => continue,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use bt_gatt::test_utils::{FakeServer, FakeTypes};
use futures::{FutureExt, StreamExt};
#[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));
}
#[test]
fn build_server_success() {
let generic_server: McsServer<FakeTypes> =
McsServerBuilder::generic(0x42, "Test Generic Player")
.build()
.expect("generic server builds successfully");
assert!(!generic_server.is_published());
assert!(generic_server.is_generic_service());
let instance_server: McsServer<FakeTypes> =
McsServerBuilder::instance(0x42, "Test Instance Player")
.build()
.expect("instance server builds successfully");
assert!(!instance_server.is_published());
assert!(!instance_server.is_generic_service());
}
#[test]
fn publish_server_success() {
let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref());
let mut server: McsServer<FakeTypes> =
McsServerBuilder::generic(0x42, "Test Generic Player")
.build()
.expect("server builds successfully");
assert!(!server.is_published());
let (fake_gatt_server, _event_receiver) = FakeServer::new();
let Poll::Pending = server.next().poll_unpin(&mut noop_cx) else {
panic!("Should be pending before publish");
};
server.publish(fake_gatt_server).expect("publish succeeds");
// Advance state: Preparing -> Published
let Poll::Pending = server.next().poll_unpin(&mut noop_cx) else {
panic!("Should be pending after publish");
};
assert!(server.is_published());
}
#[test]
fn publish_server_already_published_error() {
let (fake_gatt_server, _event_receiver) = FakeServer::new();
let mut server: McsServer<FakeTypes> = McsServerBuilder::generic(0x42, "Test Player")
.build()
.expect("server builds successfully");
server.publish(fake_gatt_server.clone()).expect("initial publish succeeds");
let err = server.publish(fake_gatt_server);
assert!(matches!(err, Err(Error::AlreadyPublished)));
}
#[test]
fn duplicate_ccid_publish_error() {
let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref());
let (fake_gatt_server, _event_receiver) = FakeServer::new();
let mut server1: McsServer<FakeTypes> = McsServerBuilder::instance(0x05, "Player 1")
.build()
.expect("server1 builds successfully");
let mut server2: McsServer<FakeTypes> = McsServerBuilder::instance(0x05, "Player 2")
.build()
.expect("server2 builds successfully");
// The first server publishes successfully.
server1.publish(fake_gatt_server.clone()).expect("server1 publish call succeeds");
let _ = server1.next().poll_unpin(&mut noop_cx);
assert!(server1.is_published());
// The GATT server rejects the second server attempting to publish with the
// duplicate CCID / ServiceId.
fake_gatt_server.set_next_prepare_result(Err(bt_gatt::types::Error::AlreadyPublished(
ServiceId::new(0x05),
)));
server2.publish(fake_gatt_server).expect("server2 publish call succeeds");
let poll_result = server2.next().poll_unpin(&mut noop_cx);
assert!(matches!(
poll_result,
Poll::Ready(Some(Err(Error::Gatt(bt_gatt::types::Error::AlreadyPublished(_)))))
));
}
#[test]
fn server_stream_terminates_when_event_stream_closes() {
let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref());
let (fake_gatt_server, _event_receiver) = FakeServer::new();
let mut server: McsServer<FakeTypes> = McsServerBuilder::generic(0x42, "Test Player")
.build()
.expect("server builds successfully");
server.publish(fake_gatt_server).expect("publish succeeds");
// Advance to Published
let Poll::Pending = server.next().poll_unpin(&mut noop_cx) else {
panic!("Should be pending after publish");
};
assert!(server.is_published());
// Replace local_service events stream with a custom channel that can be
// explicitly closed.
let (sender, receiver) = futures::channel::mpsc::unbounded();
let LocalServiceState::Published { service, .. } =
std::mem::replace(&mut server.local_service, LocalServiceState::Terminated)
else {
panic!("Expected server to be in Published state");
};
server.local_service = LocalServiceState::Published { service, events: receiver };
// Dropping the sender closes the event stream.
drop(sender);
// Polling the server returns None indicating the stream has terminated.
let poll_result = server.next().poll_unpin(&mut noop_cx);
assert!(matches!(poll_result, Poll::Ready(None)));
// Subsequent polls on terminated state also return None.
let poll_result = server.next().poll_unpin(&mut noop_cx);
assert!(matches!(poll_result, Poll::Ready(None)));
}
}