| // 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 |
| ); |
| } |
| } |