rust/bt-mcs: Support GATT reads on optional characteristics

Add builder methods to McsServerBuilder to enable optional non-OTS
features on the MCS/GMCS server.

Dynamically generate the GATT service definition based on the enabled
characteristics.

Dispatch incoming read requests for configured characteristics, and
return InvalidHandle for unconfigured optional handles.

Bug: b/540400364
Test: ./presubmit.sh
Change-Id: Ib614e74ed23ad3dd47dea02dee93628c5df6a798
Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/3840
diff --git a/rust/bt-mcs/src/server.rs b/rust/bt-mcs/src/server.rs
index f666070..be7e567 100644
--- a/rust/bt-mcs/src/server.rs
+++ b/rust/bt-mcs/src/server.rs
@@ -35,87 +35,64 @@
 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);
+const MEDIA_STATE_HANDLE: Handle = Handle(6);
 /// Handle assigned to the Content Control ID (CCID) characteristic.
-const CONTENT_CONTROL_ID_HANDLE: Handle = Handle(13);
+const CONTENT_CONTROL_ID_HANDLE: Handle = Handle(7);
 
-/// All 13 mandatory characteristics defined in MCS v1.0.1 Section 3.
-fn mandatory_characteristics() -> [Characteristic; 13] {
+// ============================================================================
+// Optional Characteristic Handle Definitions (MCS v1.0.1 Section 3)
+// ============================================================================
+
+/// Handle assigned to the Media Player Icon URL characteristic.
+const MEDIA_PLAYER_ICON_URL_HANDLE: Handle = Handle(8);
+/// Handle assigned to the Playback Speed characteristic.
+const PLAYBACK_SPEED_HANDLE: Handle = Handle(9);
+/// Handle assigned to the Seeking Speed characteristic.
+const SEEKING_SPEED_HANDLE: Handle = Handle(10);
+/// Handle assigned to the Playing Order characteristic.
+const PLAYING_ORDER_HANDLE: Handle = Handle(11);
+/// Handle assigned to the Playing Orders Supported characteristic.
+const PLAYING_ORDERS_SUPPORTED_HANDLE: Handle = Handle(12);
+/// Handle assigned to the Media Control Point characteristic.
+const MEDIA_CONTROL_POINT_HANDLE: Handle = Handle(13);
+/// Handle assigned to the Media Control Point Opcodes Supported characteristic.
+const MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_HANDLE: Handle = Handle(14);
+
+/// The mandatory characteristics defined in MCS v1.0.1 Section 3, Table 3.1.
+fn mandatory_characteristics() -> [Characteristic; 7] {
     [
-        build_mandatory_characteristic(
+        build_characteristic(
             MEDIA_PLAYER_NAME_HANDLE,
             MEDIA_PLAYER_NAME_UUID,
             CharacteristicProperties::READ_NOTIFY,
         ),
-        build_mandatory_characteristic(
+        build_characteristic(
             TRACK_CHANGED_HANDLE,
             TRACK_CHANGED_UUID,
             CharacteristicProperty::Notify,
         ),
-        build_mandatory_characteristic(
+        build_characteristic(
             TRACK_TITLE_HANDLE,
             TRACK_TITLE_UUID,
             CharacteristicProperties::READ_NOTIFY,
         ),
-        build_mandatory_characteristic(
+        build_characteristic(
             TRACK_DURATION_HANDLE,
             TRACK_DURATION_UUID,
             CharacteristicProperties::READ_NOTIFY,
         ),
-        build_mandatory_characteristic(
+        build_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(
+        build_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(
+        build_characteristic(
             CONTENT_CONTROL_ID_HANDLE,
             CONTENT_CONTROL_ID_UUID,
             CharacteristicProperty::Read,
@@ -123,11 +100,10 @@
     ]
 }
 
-/// 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(
+fn build_characteristic(
     handle: Handle,
     uuid: Uuid,
     properties: impl Into<CharacteristicProperties>,
@@ -220,7 +196,7 @@
     }
 }
 
-/// Local state of the mandatory characteristics in this server.
+/// Local state of the characteristics in this server.
 #[derive(Debug, Clone, PartialEq, Eq)]
 struct McsLocalState {
     /// Content Control ID (CCID) identifying this media service instance.
@@ -237,6 +213,17 @@
     position_updated_at: Option<std::time::Instant>,
     /// Current player activity state.
     media_state: MediaState,
+    /// URL pointing to media player icon graphic, if supported.
+    icon_url: Option<String>,
+    /// Playback speed multiplier (0 = 1.0x normal speed), if supported.
+    playback_speed: Option<i8>,
+    /// Seeking speed factor (0 = not seeking), if supported.
+    seeking_speed: Option<i8>,
+    /// Playing order and supported playing orders, if supported.
+    playing_orders: Option<PlayingOrderState>,
+    /// Supported media control point opcodes, if Media Control Point is
+    /// supported.
+    supported_opcodes: Option<SupportedOpcodes>,
 }
 
 impl McsLocalState {
@@ -251,6 +238,11 @@
             track_position: TrackPosition::Unavailable,
             position_updated_at: None,
             media_state: MediaState::Inactive,
+            icon_url: None,
+            playback_speed: None,
+            seeking_speed: None,
+            playing_orders: None,
+            supported_opcodes: None,
         }
     }
 
@@ -291,14 +283,12 @@
 
     /// Reads the characteristic bytes for `handle` at `offset`.
     fn handle_read(&self, handle: Handle, offset: usize) -> Result<Vec<u8>, GattError> {
-        // Track Changed is notify-only. Return Error. See MCS v1.0.1 Section 3.4.
-        if handle == TRACK_CHANGED_HANDLE {
-            return Err(GattError::ReadNotPermitted);
-        }
-
         let read_at_offset =
             |bytes: &[u8]| bytes.get(offset..).map(Vec::from).ok_or(GattError::InvalidOffset);
         match handle {
+            // Track Changed and Media Control Point are not readable per MCS v1.0.1 Section 3,
+            // Table 3.1.
+            TRACK_CHANGED_HANDLE | MEDIA_CONTROL_POINT_HANDLE => Err(GattError::ReadNotPermitted),
             MEDIA_PLAYER_NAME_HANDLE => read_at_offset(self.player_name.as_bytes()),
             TRACK_TITLE_HANDLE => read_at_offset(self.track_title.as_bytes()),
             TRACK_DURATION_HANDLE => read_at_offset(&self.track_duration.raw_10ms().to_le_bytes()),
@@ -307,6 +297,30 @@
             }
             MEDIA_STATE_HANDLE => read_at_offset(&[self.media_state.into()]),
             CONTENT_CONTROL_ID_HANDLE => read_at_offset(&[self.ccid]),
+            MEDIA_PLAYER_ICON_URL_HANDLE => {
+                let url = self.icon_url.as_ref().ok_or(GattError::InvalidHandle)?;
+                read_at_offset(url.as_bytes())
+            }
+            PLAYBACK_SPEED_HANDLE => {
+                let speed = self.playback_speed.ok_or(GattError::InvalidHandle)?;
+                read_at_offset(&[speed as u8])
+            }
+            SEEKING_SPEED_HANDLE => {
+                let speed = self.seeking_speed.ok_or(GattError::InvalidHandle)?;
+                read_at_offset(&[speed as u8])
+            }
+            PLAYING_ORDER_HANDLE => {
+                let orders = self.playing_orders.as_ref().ok_or(GattError::InvalidHandle)?;
+                read_at_offset(&[orders.current.into()])
+            }
+            PLAYING_ORDERS_SUPPORTED_HANDLE => {
+                let orders = self.playing_orders.as_ref().ok_or(GattError::InvalidHandle)?;
+                read_at_offset(&orders.supported.bits().to_le_bytes())
+            }
+            MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_HANDLE => {
+                let opcodes = self.supported_opcodes.ok_or(GattError::InvalidHandle)?;
+                read_at_offset(&opcodes.bits().to_le_bytes())
+            }
             _ => Err(GattError::InvalidHandle),
         }
     }
@@ -337,8 +351,34 @@
         Self { service_uuid, state: McsLocalState::new(ccid, player_name) }
     }
 
-    /// Constructs the complete GATT [`ServiceDefinition`] containing all 13
-    /// Mandatory Characteristics defined in MCS v1.0.1 Section 3.
+    /// Enables media player icon URL support.
+    pub fn with_icon_url(mut self, url: impl Into<String>) -> Self {
+        self.state.icon_url = Some(url.into());
+        self
+    }
+
+    /// Enables playback and seeking speed support with default speeds.
+    pub fn with_player_speeds(mut self) -> Self {
+        self.state.playback_speed = Some(0);
+        self.state.seeking_speed = Some(0);
+        self
+    }
+
+    /// Enables playing order support with the given supported playing orders.
+    pub fn with_playing_orders(mut self, supported: SupportedPlayingOrders) -> Self {
+        self.state.playing_orders = Some(PlayingOrderState::new(supported));
+        self
+    }
+
+    /// Enables media control operations with the given supported opcodes.
+    pub fn with_supported_operations(mut self, supported: SupportedOpcodes) -> Self {
+        self.state.supported_opcodes = Some(supported);
+        self
+    }
+
+    /// Constructs the GATT [`ServiceDefinition`] containing the mandatory
+    /// characteristics per MCS v1.0.1 Section 3 and any configured optional
+    /// characteristics.
     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
@@ -354,6 +394,52 @@
             service_def.add_characteristic(chrc)?;
         }
 
+        if self.state.icon_url.is_some() {
+            service_def.add_characteristic(build_characteristic(
+                MEDIA_PLAYER_ICON_URL_HANDLE,
+                MEDIA_PLAYER_ICON_URL_UUID,
+                CharacteristicProperty::Read,
+            ))?;
+        }
+        if self.state.playback_speed.is_some() {
+            service_def.add_characteristic(build_characteristic(
+                PLAYBACK_SPEED_HANDLE,
+                PLAYBACK_SPEED_UUID,
+                CharacteristicProperties::READ_WRITE_NOTIFY,
+            ))?;
+        }
+        if self.state.seeking_speed.is_some() {
+            service_def.add_characteristic(build_characteristic(
+                SEEKING_SPEED_HANDLE,
+                SEEKING_SPEED_UUID,
+                CharacteristicProperties::READ_NOTIFY,
+            ))?;
+        }
+        if self.state.playing_orders.is_some() {
+            service_def.add_characteristic(build_characteristic(
+                PLAYING_ORDER_HANDLE,
+                PLAYING_ORDER_UUID,
+                CharacteristicProperties::READ_WRITE_NOTIFY,
+            ))?;
+            service_def.add_characteristic(build_characteristic(
+                PLAYING_ORDERS_SUPPORTED_HANDLE,
+                PLAYING_ORDERS_SUPPORTED_UUID,
+                CharacteristicProperty::Read,
+            ))?;
+        }
+        if self.state.supported_opcodes.is_some() {
+            service_def.add_characteristic(build_characteristic(
+                MEDIA_CONTROL_POINT_HANDLE,
+                MEDIA_CONTROL_POINT_UUID,
+                CharacteristicProperties::WRITE_NOTIFY,
+            ))?;
+            service_def.add_characteristic(build_characteristic(
+                MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_HANDLE,
+                MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_UUID,
+                CharacteristicProperties::READ_NOTIFY,
+            ))?;
+        }
+
         Ok(service_def)
     }
 
@@ -454,7 +540,7 @@
         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);
+        assert_eq!(service_def.characteristics().count(), 7);
     }
 
     #[test]
@@ -466,7 +552,74 @@
         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);
+        assert_eq!(service_def.characteristics().count(), 7);
+    }
+
+    #[test]
+    fn builder_with_optional_characteristics_service_definition() {
+        let builder = McsServerBuilder::generic(0x10, "Full Player")
+            .with_icon_url("https://example.com/icon.png")
+            .with_player_speeds()
+            .with_playing_orders(SupportedPlayingOrders::default())
+            .with_supported_operations(SupportedOpcodes::default());
+
+        let service_def =
+            builder.build_service_definition().expect("service definition builds successfully");
+        assert_eq!(service_def.characteristics().count(), 14);
+
+        let characteristics: Vec<&Characteristic> = service_def.characteristics().collect();
+        let expected_characteristics = [
+            mandatory_characteristics()[0].clone(),
+            mandatory_characteristics()[1].clone(),
+            mandatory_characteristics()[2].clone(),
+            mandatory_characteristics()[3].clone(),
+            mandatory_characteristics()[4].clone(),
+            mandatory_characteristics()[5].clone(),
+            mandatory_characteristics()[6].clone(),
+            build_characteristic(
+                MEDIA_PLAYER_ICON_URL_HANDLE,
+                MEDIA_PLAYER_ICON_URL_UUID,
+                CharacteristicProperty::Read,
+            ),
+            build_characteristic(
+                PLAYBACK_SPEED_HANDLE,
+                PLAYBACK_SPEED_UUID,
+                CharacteristicProperties::READ_WRITE_NOTIFY,
+            ),
+            build_characteristic(
+                SEEKING_SPEED_HANDLE,
+                SEEKING_SPEED_UUID,
+                CharacteristicProperties::READ_NOTIFY,
+            ),
+            build_characteristic(
+                PLAYING_ORDER_HANDLE,
+                PLAYING_ORDER_UUID,
+                CharacteristicProperties::READ_WRITE_NOTIFY,
+            ),
+            build_characteristic(
+                PLAYING_ORDERS_SUPPORTED_HANDLE,
+                PLAYING_ORDERS_SUPPORTED_UUID,
+                CharacteristicProperty::Read,
+            ),
+            build_characteristic(
+                MEDIA_CONTROL_POINT_HANDLE,
+                MEDIA_CONTROL_POINT_UUID,
+                CharacteristicProperties::WRITE_NOTIFY,
+            ),
+            build_characteristic(
+                MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_HANDLE,
+                MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_UUID,
+                CharacteristicProperties::READ_NOTIFY,
+            ),
+        ];
+
+        assert_eq!(characteristics.len(), expected_characteristics.len());
+        for (i, expected) in expected_characteristics.iter().enumerate() {
+            let chrc = characteristics[i];
+            assert_eq!(chrc.handle, expected.handle);
+            assert_eq!(chrc.uuid, expected.uuid);
+            assert_eq!(chrc.properties, expected.properties);
+        }
     }
 
     #[test]
@@ -677,7 +830,7 @@
         let (mut server, fake_gatt_server, mut event_receiver) =
             setup_test_server(McsServerBuilder::generic(0x42, "Test Player"));
 
-        // Media Player Name (0x2B93)
+        // Media Player Name
         assert_read_characteristic(
             &mut server,
             &fake_gatt_server,
@@ -686,7 +839,7 @@
             b"Test Player",
         );
 
-        // Track Title (0x2B97)
+        // Track Title
         assert_read_characteristic(
             &mut server,
             &fake_gatt_server,
@@ -695,7 +848,7 @@
             b"",
         );
 
-        // Track Duration (0x2B98)
+        // Track Duration
         assert_read_characteristic(
             &mut server,
             &fake_gatt_server,
@@ -704,7 +857,7 @@
             &(-1i32).to_le_bytes(),
         );
 
-        // Track Position (0x2B99)
+        // Track Position
         assert_read_characteristic(
             &mut server,
             &fake_gatt_server,
@@ -713,7 +866,7 @@
             &(-1i32).to_le_bytes(),
         );
 
-        // Media State (0x2BA3)
+        // Media State
         assert_read_characteristic(
             &mut server,
             &fake_gatt_server,
@@ -722,7 +875,7 @@
             &[MediaState::Inactive.into()],
         );
 
-        // Content Control ID (0x2BA8)
+        // Content Control ID
         assert_read_characteristic(
             &mut server,
             &fake_gatt_server,
@@ -733,15 +886,121 @@
     }
 
     #[test]
-    fn read_string_characteristics_with_offset() {
-        let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref());
+    fn read_optional_characteristics_configured_values() {
+        let (mut server, fake_gatt_server, mut event_receiver) = setup_test_server(
+            McsServerBuilder::generic(0x42, "Test Player")
+                .with_icon_url("https://example.com/icon.png")
+                .with_player_speeds()
+                .with_playing_orders(
+                    SupportedPlayingOrders::IN_ORDER_ONCE | SupportedPlayingOrders::SHUFFLE_ONCE,
+                )
+                .with_supported_operations(SupportedOpcodes::PLAY | SupportedOpcodes::PAUSE),
+        );
+
+        // Media Player Icon URL
+        assert_read_characteristic(
+            &mut server,
+            &fake_gatt_server,
+            &mut event_receiver,
+            MEDIA_PLAYER_ICON_URL_HANDLE,
+            b"https://example.com/icon.png",
+        );
+
+        // Playback Speed
+        assert_read_characteristic(
+            &mut server,
+            &fake_gatt_server,
+            &mut event_receiver,
+            PLAYBACK_SPEED_HANDLE,
+            &[0],
+        );
+
+        // Seeking Speed
+        assert_read_characteristic(
+            &mut server,
+            &fake_gatt_server,
+            &mut event_receiver,
+            SEEKING_SPEED_HANDLE,
+            &[0],
+        );
+
+        // Playing Order
+        assert_read_characteristic(
+            &mut server,
+            &fake_gatt_server,
+            &mut event_receiver,
+            PLAYING_ORDER_HANDLE,
+            &[PlayingOrder::InOrderOnce.into()],
+        );
+
+        // Playing Orders Supported
+        let expected_orders =
+            SupportedPlayingOrders::IN_ORDER_ONCE | SupportedPlayingOrders::SHUFFLE_ONCE;
+        assert_read_characteristic(
+            &mut server,
+            &fake_gatt_server,
+            &mut event_receiver,
+            PLAYING_ORDERS_SUPPORTED_HANDLE,
+            &expected_orders.bits().to_le_bytes(),
+        );
+
+        // Media Control Point Opcodes Supported
+        let expected_opcodes = SupportedOpcodes::PLAY | SupportedOpcodes::PAUSE;
+        assert_read_characteristic(
+            &mut server,
+            &fake_gatt_server,
+            &mut event_receiver,
+            MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_HANDLE,
+            &expected_opcodes.bits().to_le_bytes(),
+        );
+    }
+
+    #[test]
+    fn read_unconfigured_optional_characteristics_returns_invalid_handle() {
         let (mut server, fake_gatt_server, mut event_receiver) =
-            setup_test_server(McsServerBuilder::generic(0x42, "Long Player Name"));
+            setup_test_server(McsServerBuilder::generic(0x42, "Mandatory Only Player"));
 
+        let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref());
         let peer = PeerId(1);
-        let service_id = ServiceId::new(0x42);
+        let service_id = server.service_def.id();
 
-        // Valid offset slice
+        let unconfigured_handles = [
+            MEDIA_PLAYER_ICON_URL_HANDLE,
+            PLAYBACK_SPEED_HANDLE,
+            SEEKING_SPEED_HANDLE,
+            PLAYING_ORDER_HANDLE,
+            PLAYING_ORDERS_SUPPORTED_HANDLE,
+            MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_HANDLE,
+        ];
+
+        for handle in unconfigured_handles {
+            fake_gatt_server.incoming_read(peer, service_id, handle, 0);
+            let _ = server.next().poll_unpin(&mut noop_cx);
+            let bt_gatt::test_utils::FakeServerEvent::ReadResponded { value, .. } =
+                event_receiver.try_recv().unwrap()
+            else {
+                panic!("expected ReadResponded for handle {:?}", handle);
+            };
+            assert!(
+                matches!(value.unwrap_err(), bt_gatt::types::Error::Gatt(GattError::InvalidHandle)),
+                "handle {:?} should return InvalidHandle when unconfigured",
+                handle
+            );
+        }
+    }
+
+    #[test]
+    fn read_string_characteristics_with_offset() {
+        let (mut server, fake_gatt_server, mut event_receiver) = setup_test_server(
+            McsServerBuilder::generic(0x42, "Long Player Name")
+                .with_icon_url("https://example.com/icon.png"),
+        );
+
+        let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref());
+        let peer = PeerId(1);
+        let service_id = server.service_def.id();
+
+        // Valid offset slice for Player Name
         fake_gatt_server.incoming_read(peer, service_id, MEDIA_PLAYER_NAME_HANDLE, 5);
         let _ = server.next().poll_unpin(&mut noop_cx);
         let bt_gatt::test_utils::FakeServerEvent::ReadResponded { value, .. } =
@@ -751,6 +1010,16 @@
         };
         assert_eq!(value.unwrap(), b"Player Name");
 
+        // Valid offset slice for Icon URL
+        fake_gatt_server.incoming_read(peer, service_id, MEDIA_PLAYER_ICON_URL_HANDLE, 8);
+        let _ = server.next().poll_unpin(&mut noop_cx);
+        let bt_gatt::test_utils::FakeServerEvent::ReadResponded { value, .. } =
+            event_receiver.try_recv().unwrap()
+        else {
+            panic!("expected ReadResponded");
+        };
+        assert_eq!(value.unwrap(), b"example.com/icon.png");
+
         // Exact end offset returns empty slice
         fake_gatt_server.incoming_read(
             peer,
@@ -778,16 +1047,31 @@
             value.unwrap_err(),
             bt_gatt::types::Error::Gatt(GattError::InvalidOffset)
         ));
+
+        // Out-of-bounds offset on Icon URL returns InvalidOffset
+        fake_gatt_server.incoming_read(peer, service_id, MEDIA_PLAYER_ICON_URL_HANDLE, 100);
+        let _ = server.next().poll_unpin(&mut noop_cx);
+        let bt_gatt::test_utils::FakeServerEvent::ReadResponded { value, .. } =
+            event_receiver.try_recv().unwrap()
+        else {
+            panic!("expected ReadResponded");
+        };
+        assert!(matches!(
+            value.unwrap_err(),
+            bt_gatt::types::Error::Gatt(GattError::InvalidOffset)
+        ));
     }
 
     #[test]
     fn read_non_readable_characteristics_returns_error() {
         let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref());
-        let (mut server, fake_gatt_server, mut event_receiver) =
-            setup_test_server(McsServerBuilder::generic(0x42, "Test Player"));
+        let (mut server, fake_gatt_server, mut event_receiver) = setup_test_server(
+            McsServerBuilder::generic(0x42, "Test Player")
+                .with_supported_operations(SupportedOpcodes::default()),
+        );
 
         let peer = PeerId(1);
-        let service_id = ServiceId::new(0x42);
+        let service_id = server.service_def.id();
 
         // Track Changed is notify-only
         fake_gatt_server.incoming_read(peer, service_id, TRACK_CHANGED_HANDLE, 0);
@@ -802,6 +1086,19 @@
             bt_gatt::types::Error::Gatt(GattError::ReadNotPermitted)
         ));
 
+        // Media Control Point is write/notify-only
+        fake_gatt_server.incoming_read(peer, service_id, MEDIA_CONTROL_POINT_HANDLE, 0);
+        let _ = server.next().poll_unpin(&mut noop_cx);
+        let bt_gatt::test_utils::FakeServerEvent::ReadResponded { value, .. } =
+            event_receiver.try_recv().unwrap()
+        else {
+            panic!("expected ReadResponded");
+        };
+        assert!(matches!(
+            value.unwrap_err(),
+            bt_gatt::types::Error::Gatt(GattError::ReadNotPermitted)
+        ));
+
         // Unknown handle returns InvalidHandle
         fake_gatt_server.incoming_read(peer, service_id, Handle(999), 0);
         let _ = server.next().poll_unpin(&mut noop_cx);
diff --git a/rust/bt-mcs/src/types.rs b/rust/bt-mcs/src/types.rs
index 30043ac..1d9d8d0 100644
--- a/rust/bt-mcs/src/types.rs
+++ b/rust/bt-mcs/src/types.rs
@@ -20,6 +20,8 @@
 
 /// Media Player Name characteristic UUID.
 pub(crate) const MEDIA_PLAYER_NAME_UUID: Uuid = Uuid::from_u16(0x2B93);
+/// Media Player Icon URL characteristic UUID.
+pub(crate) const MEDIA_PLAYER_ICON_URL_UUID: Uuid = Uuid::from_u16(0x2B95);
 /// Track Changed characteristic UUID.
 pub(crate) const TRACK_CHANGED_UUID: Uuid = Uuid::from_u16(0x2B96);
 /// Track Title characteristic UUID.
@@ -46,7 +48,7 @@
 pub(crate) const CONTENT_CONTROL_ID_UUID: Uuid = Uuid::from_u16(0x2BBA);
 
 decodable_enum! {
-    /// State of the media player.
+    /// State of the media player (MCS v1.0.1 Section 3.17, Table 3.5).
     pub enum MediaState<u8, PacketError, OutOfRange> {
         /// Media player is inactive.
         Inactive = 0x00,
@@ -87,7 +89,7 @@
 }
 
 decodable_enum! {
-    /// Result code returned in a Media Control Point notification.
+    /// Result code returned in a Media Control Point notification (MCS v1.0.1 Section 3.18.2, Table 3.8).
     pub enum ControlPointResultCode<u8, PacketError, OutOfRange> {
         /// The procedure completed successfully.
         Success = 0x01,
@@ -180,6 +182,30 @@
     }
 }
 
+impl Default for PlayingOrder {
+    /// Returns standard sequential playing order (`InOrderOnce`) by default.
+    fn default() -> Self {
+        Self::InOrderOnce
+    }
+}
+
+/// State of the playing order characteristics when enabled.
+#[derive(Debug, Default, Clone, PartialEq, Eq)]
+pub struct PlayingOrderState {
+    /// Currently selected playing order.
+    pub current: PlayingOrder,
+    /// Bitmask of supported playing orders.
+    pub supported: SupportedPlayingOrders,
+}
+
+impl PlayingOrderState {
+    /// Creates a new `PlayingOrderState` with the default current order
+    /// (`PlayingOrder::InOrderOnce`) and the given supported bitmask.
+    pub fn new(supported: SupportedPlayingOrders) -> Self {
+        Self { current: PlayingOrder::default(), supported }
+    }
+}
+
 bitflags! {
     /// Bitmask representing supported playing orders (MCS v1.0.1 Section 3.16, Table 3.4).
     #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -437,7 +463,7 @@
 }
 
 bitflags! {
-    /// Bitmask representing supported Media Control Point opcodes (MCS v1.0.1 Section 3.19, Table 3.10).
+    /// Bitmask representing supported Media Control Point opcodes (MCS v1.0.1 Section 3.19, Table 3.9).
     #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     pub struct SupportedOpcodes: u32 {
         const PLAY = 0x00000001;
@@ -873,6 +899,17 @@
     }
 
     #[test]
+    fn playing_order_state_defaults() {
+        let default_state = PlayingOrderState::default();
+        assert_eq!(default_state.current, PlayingOrder::InOrderOnce);
+        assert_eq!(default_state.supported, SupportedPlayingOrders::IN_ORDER_ONCE);
+
+        let custom_state = PlayingOrderState::new(SupportedPlayingOrders::all());
+        assert_eq!(custom_state.current, PlayingOrder::InOrderOnce);
+        assert_eq!(custom_state.supported, SupportedPlayingOrders::all());
+    }
+
+    #[test]
     fn media_control_opcode_all_non_parameterized() {
         let opcodes = [
             (MediaControlOpcode::Play, 0x01),