blob: 26fed15f0aa810ff2b49c7971114b4e754180f30 [file] [edit]
// 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
);
}
}