| // 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. | |
| use bt_common::PeerId; | |
| use futures::channel::oneshot; | |
| use futures::FutureExt; | |
| use std::future::Future; | |
| use std::pin::Pin; | |
| use std::task::{Context, Poll}; | |
| use crate::types::*; | |
| /// Confirms the updated track position to store in local state and notify to | |
| /// clients. | |
| /// It is safe to drop without responding to ignore the update or if the value | |
| /// has not changed. | |
| pub type SetTrackPositionResponder = SetValueResponder<TrackPosition>; | |
| /// Confirms the updated playback speed to store in local state and notify to | |
| /// clients. | |
| /// It is safe to drop without responding to ignore the update or if the value | |
| /// has not changed. | |
| pub type SetPlaybackSpeedResponder = SetValueResponder<PlaybackSpeed>; | |
| /// Confirms the updated playing order to store in local state and notify to | |
| /// clients. | |
| /// It is safe to drop without responding to ignore the update or if the value | |
| /// has not changed. | |
| pub type SetPlayingOrderResponder = SetValueResponder<PlayingOrder>; | |
| /// Confirms the media control command result code to notify to the client. | |
| /// A response is expected to confirm the result. If no response is provided, | |
| /// then it is assumed that the Control Point request has failed and | |
| /// `ControlPointResultCode::CommandCannotBeCompleted` will be sent to the peer. | |
| pub type ControlPointWriteResponder = SetValueResponder<ControlPointResultCode>; | |
| /// Responder for an asynchronous characteristic write or control point command. | |
| /// | |
| /// Calling [`send`](Self::send) confirms the new value or command result to | |
| /// update local state and notify clients. It is safe to drop the responder | |
| /// without responding to cancel the update (or report command failure). | |
| pub struct SetValueResponder<T> { | |
| response_tx: oneshot::Sender<T>, | |
| } | |
| impl<T: std::fmt::Debug> SetValueResponder<T> { | |
| /// Confirms the value or result code determined by the media player | |
| /// application and dispatches the corresponding GATT notification. | |
| pub fn send(self, value: T) { | |
| if let Err(result) = self.response_tx.send(value) { | |
| log::warn!("Failed to send response: server dropped or closed: {result:?}"); | |
| } | |
| } | |
| } | |
| impl<T> std::fmt::Debug for SetValueResponder<T> { | |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| f.debug_struct("SetValueResponder").finish_non_exhaustive() | |
| } | |
| } | |
| /// An in-flight handler for a single-value asynchronous write response. | |
| pub(crate) struct SetValueResponseFut<T> { | |
| rx: oneshot::Receiver<T>, | |
| } | |
| impl<T> SetValueResponseFut<T> { | |
| pub(crate) fn create() -> (Self, SetValueResponder<T>) { | |
| let (tx, rx) = oneshot::channel(); | |
| (Self { rx }, SetValueResponder { response_tx: tx }) | |
| } | |
| } | |
| impl<T> Future for SetValueResponseFut<T> { | |
| type Output = Option<T>; | |
| fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { | |
| self.rx.poll_unpin(cx).map(Result::ok) | |
| } | |
| } | |
| /// Responses produced when an in-flight asynchronous write response completes. | |
| #[derive(Debug)] | |
| pub(crate) enum PendingWriteResponse { | |
| ControlPoint { | |
| peer_id: PeerId, | |
| opcode: MediaControlOpcode, | |
| result_code: ControlPointResultCode, | |
| }, | |
| TrackPosition(TrackPosition), | |
| PlaybackSpeed(PlaybackSpeed), | |
| PlayingOrder(PlayingOrder), | |
| } | |
| /// An in-flight asynchronous write response future. | |
| pub(crate) enum PendingWriteResponseFut { | |
| ControlPoint { | |
| peer_id: PeerId, | |
| opcode: MediaControlOpcode, | |
| fut: SetValueResponseFut<ControlPointResultCode>, | |
| }, | |
| TrackPosition(SetValueResponseFut<TrackPosition>), | |
| PlaybackSpeed(SetValueResponseFut<PlaybackSpeed>), | |
| PlayingOrder(SetValueResponseFut<PlayingOrder>), | |
| } | |
| impl Future for PendingWriteResponseFut { | |
| type Output = Option<PendingWriteResponse>; | |
| fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { | |
| match self.as_mut().get_mut() { | |
| Self::ControlPoint { peer_id, opcode, fut } => fut.poll_unpin(cx).map(|res| { | |
| let code = res.unwrap_or(ControlPointResultCode::CommandCannotBeCompleted); | |
| Some(PendingWriteResponse::ControlPoint { | |
| peer_id: *peer_id, | |
| opcode: opcode.clone(), | |
| result_code: code, | |
| }) | |
| }), | |
| Self::TrackPosition(fut) => { | |
| fut.poll_unpin(cx).map(|res| res.map(PendingWriteResponse::TrackPosition)) | |
| } | |
| Self::PlaybackSpeed(fut) => { | |
| fut.poll_unpin(cx).map(|res| res.map(PendingWriteResponse::PlaybackSpeed)) | |
| } | |
| Self::PlayingOrder(fut) => { | |
| fut.poll_unpin(cx).map(|res| res.map(PendingWriteResponse::PlayingOrder)) | |
| } | |
| } | |
| } | |
| } | |
| /// Events produced by an [`McsServer`] stream representing client actions that | |
| /// require additional upper-layer application processing. | |
| #[derive(Debug)] | |
| pub enum McsServerEvent { | |
| /// Request to set the track playback position. | |
| SetTrackPosition { | |
| peer_id: PeerId, | |
| position: TrackPosition, | |
| responder: SetValueResponder<TrackPosition>, | |
| }, | |
| /// Request to set the playback speed. | |
| SetPlaybackSpeed { | |
| peer_id: PeerId, | |
| speed: PlaybackSpeed, | |
| responder: SetValueResponder<PlaybackSpeed>, | |
| }, | |
| /// Request to set the playing order. | |
| SetPlayingOrder { | |
| peer_id: PeerId, | |
| order: PlayingOrder, | |
| responder: SetValueResponder<PlayingOrder>, | |
| }, | |
| /// Request to execute a media control command. | |
| ControlPointCommand { | |
| opcode: MediaControlOpcode, | |
| responder: SetValueResponder<ControlPointResultCode>, | |
| }, | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use assert_matches::assert_matches; | |
| #[test] | |
| fn set_value_responder_and_fut() { | |
| let (mut fut, responder) = SetValueResponseFut::<PlaybackSpeed>::create(); | |
| let mut cx = Context::from_waker(futures::task::noop_waker_ref()); | |
| assert!(fut.poll_unpin(&mut cx).is_pending()); | |
| responder.send(PlaybackSpeed::DOUBLE); | |
| assert_eq!(fut.poll_unpin(&mut cx), Poll::Ready(Some(PlaybackSpeed::DOUBLE))); | |
| } | |
| #[test] | |
| fn set_value_responder_dropped_results_in_none() { | |
| let (mut fut, responder) = SetValueResponseFut::<TrackPosition>::create(); | |
| let mut cx = Context::from_waker(futures::task::noop_waker_ref()); | |
| assert!(fut.poll_unpin(&mut cx).is_pending()); | |
| drop(responder); | |
| assert_eq!(fut.poll_unpin(&mut cx), Poll::Ready(None)); | |
| } | |
| #[test] | |
| fn pending_write_response_control_point_dropped_defaults_to_cannot_be_completed() { | |
| let (fut, responder) = SetValueResponseFut::<ControlPointResultCode>::create(); | |
| let peer_id = PeerId(1); | |
| let mut pending = PendingWriteResponseFut::ControlPoint { | |
| peer_id, | |
| opcode: MediaControlOpcode::Play, | |
| fut, | |
| }; | |
| let mut cx = Context::from_waker(futures::task::noop_waker_ref()); | |
| assert!(pending.poll_unpin(&mut cx).is_pending()); | |
| drop(responder); | |
| assert_matches!( | |
| pending.poll_unpin(&mut cx), | |
| Poll::Ready(Some(PendingWriteResponse::ControlPoint { | |
| peer_id: p, | |
| opcode: MediaControlOpcode::Play, | |
| result_code: ControlPointResultCode::CommandCannotBeCompleted, | |
| })) if p == peer_id | |
| ); | |
| } | |
| } |