1 use crate::btif::{BluetoothInterface, BtStatus, RawAddress, ToggleableProfile}; 2 use crate::topstack::get_dispatchers; 3 4 use bitflags::bitflags; 5 use num_derive::{FromPrimitive, ToPrimitive}; 6 use num_traits::cast::FromPrimitive; 7 use std::convert::{TryFrom, TryInto}; 8 use std::fmt::{Debug, Formatter}; 9 use std::sync::{Arc, Mutex}; 10 use topshim_macros::{cb_variant, log_args, profile_enabled_or, profile_enabled_or_default}; 11 12 use log::warn; 13 14 #[derive(Debug, FromPrimitive, ToPrimitive, PartialEq, PartialOrd, Clone)] 15 #[repr(u32)] 16 pub enum BtavConnectionState { 17 Disconnected = 0, 18 Connecting, 19 Connected, 20 Disconnecting, 21 } 22 23 impl From<u32> for BtavConnectionState { from(item: u32) -> Self24 fn from(item: u32) -> Self { 25 BtavConnectionState::from_u32(item).unwrap() 26 } 27 } 28 29 #[derive(Debug, FromPrimitive, PartialEq, PartialOrd)] 30 #[repr(u32)] 31 pub enum BtavAudioState { 32 RemoteSuspend = 0, 33 Stopped, 34 Started, 35 } 36 37 impl From<u32> for BtavAudioState { from(item: u32) -> Self38 fn from(item: u32) -> Self { 39 BtavAudioState::from_u32(item).unwrap() 40 } 41 } 42 43 #[derive(Debug, FromPrimitive, ToPrimitive, PartialEq, PartialOrd)] 44 #[repr(i32)] 45 pub enum A2dpCodecIndex { 46 SrcSbc = 0, 47 SrcAac, 48 SrcAptx, 49 SrcAptxHD, 50 SrcLdac, 51 SinkSbc, 52 SinkAac, 53 SinkLdac, 54 Max, 55 } 56 57 impl A2dpCodecIndex { 58 pub const SRC_MIN: A2dpCodecIndex = A2dpCodecIndex::SrcSbc; 59 pub const SRC_MAX: A2dpCodecIndex = A2dpCodecIndex::SinkSbc; 60 pub const SINK_MIN: A2dpCodecIndex = A2dpCodecIndex::SinkSbc; 61 pub const SINK_MAX: A2dpCodecIndex = A2dpCodecIndex::Max; 62 pub const MAX: A2dpCodecIndex = A2dpCodecIndex::Max; 63 pub const MIN: A2dpCodecIndex = A2dpCodecIndex::SrcSbc; 64 } 65 66 impl From<i32> for A2dpCodecIndex { from(item: i32) -> Self67 fn from(item: i32) -> Self { 68 A2dpCodecIndex::from_i32(item).unwrap_or_else(|| A2dpCodecIndex::MIN) 69 } 70 } 71 72 #[derive(Debug, FromPrimitive, ToPrimitive, PartialEq, PartialOrd)] 73 #[repr(i32)] 74 pub enum A2dpCodecPriority { 75 Disabled = -1, 76 Default = 0, 77 Highest = 1_000_000, 78 } 79 80 impl From<i32> for A2dpCodecPriority { from(item: i32) -> Self81 fn from(item: i32) -> Self { 82 A2dpCodecPriority::from_i32(item).unwrap_or_else(|| A2dpCodecPriority::Default) 83 } 84 } 85 86 #[derive(Debug)] 87 pub struct A2dpError { 88 /// Standard BT status come from a function return or the cloest approximation to the real 89 /// error. 90 pub status: BtStatus, 91 /// An additional value to help explain the error. In the A2DP context, this is often referring 92 /// to the BTA_AV_XXX status. 93 pub error: i32, 94 /// An optional error message that the lower layer wants to deliver. 95 pub error_message: Option<String>, 96 } 97 98 bitflags! { 99 #[derive(Clone, Copy, Debug, PartialEq)] 100 pub struct A2dpCodecSampleRate: i32 { 101 const RATE_NONE = 0x0; 102 const RATE_44100 = 0x01; 103 const RATE_48000 = 0x02; 104 const RATE_88200 = 0x04; 105 const RATE_96000 = 0x08; 106 const RATE_176400 = 0x10; 107 const RATE_192000 = 0x20; 108 const RATE_16000 = 0x40; 109 const RATE_24000 = 0x80; 110 } 111 } 112 113 impl A2dpCodecSampleRate { validate_bits(val: i32) -> bool114 pub fn validate_bits(val: i32) -> bool { 115 val <= A2dpCodecSampleRate::all().bits() 116 } 117 } 118 119 impl TryInto<i32> for A2dpCodecSampleRate { 120 type Error = (); try_into(self) -> Result<i32, Self::Error>121 fn try_into(self) -> Result<i32, Self::Error> { 122 Ok(self.bits()) 123 } 124 } 125 126 impl TryFrom<i32> for A2dpCodecSampleRate { 127 type Error = (); try_from(val: i32) -> Result<Self, Self::Error>128 fn try_from(val: i32) -> Result<Self, Self::Error> { 129 Self::from_bits(val).ok_or(()) 130 } 131 } 132 133 bitflags! { 134 #[derive(Clone, Copy, Debug, PartialEq)] 135 pub struct A2dpCodecBitsPerSample: i32 { 136 const SAMPLE_NONE = 0x0; 137 const SAMPLE_16 = 0x01; 138 const SAMPLE_24 = 0x02; 139 const SAMPLE_32 = 0x04; 140 } 141 } 142 143 impl A2dpCodecBitsPerSample { validate_bits(val: i32) -> bool144 pub fn validate_bits(val: i32) -> bool { 145 val <= A2dpCodecBitsPerSample::all().bits() 146 } 147 } 148 149 impl TryInto<i32> for A2dpCodecBitsPerSample { 150 type Error = (); try_into(self) -> Result<i32, Self::Error>151 fn try_into(self) -> Result<i32, Self::Error> { 152 Ok(self.bits()) 153 } 154 } 155 156 impl TryFrom<i32> for A2dpCodecBitsPerSample { 157 type Error = (); try_from(val: i32) -> Result<Self, Self::Error>158 fn try_from(val: i32) -> Result<Self, Self::Error> { 159 Self::from_bits(val).ok_or(()) 160 } 161 } 162 163 bitflags! { 164 #[derive(Clone, Copy, Debug, PartialEq)] 165 pub struct A2dpCodecChannelMode: i32 { 166 const MODE_NONE = 0x0; 167 const MODE_MONO = 0x01; 168 const MODE_STEREO = 0x02; 169 } 170 } 171 172 impl A2dpCodecChannelMode { validate_bits(val: i32) -> bool173 pub fn validate_bits(val: i32) -> bool { 174 val <= A2dpCodecChannelMode::all().bits() 175 } 176 } 177 178 impl TryInto<i32> for A2dpCodecChannelMode { 179 type Error = (); try_into(self) -> Result<i32, Self::Error>180 fn try_into(self) -> Result<i32, Self::Error> { 181 Ok(self.bits()) 182 } 183 } 184 185 impl TryFrom<i32> for A2dpCodecChannelMode { 186 type Error = (); try_from(val: i32) -> Result<Self, Self::Error>187 fn try_from(val: i32) -> Result<Self, Self::Error> { 188 Self::from_bits(val).ok_or(()) 189 } 190 } 191 192 #[cxx::bridge(namespace = bluetooth::topshim::rust)] 193 pub mod ffi { 194 unsafe extern "C++" { 195 include!("types/raw_address.h"); 196 #[namespace = ""] 197 type RawAddress = crate::btif::RawAddress; 198 } 199 200 #[derive(Debug, Copy, Clone)] 201 pub struct A2dpCodecConfig { 202 pub codec_type: i32, 203 pub codec_priority: i32, 204 pub sample_rate: i32, 205 pub bits_per_sample: i32, 206 pub channel_mode: i32, 207 pub codec_specific_1: i64, 208 pub codec_specific_2: i64, 209 pub codec_specific_3: i64, 210 pub codec_specific_4: i64, 211 } 212 213 #[derive(Debug, Default)] 214 pub struct RustPresentationPosition { 215 remote_delay_report_ns: u64, 216 total_bytes_read: u64, 217 data_position_sec: i64, 218 data_position_nsec: i32, 219 } 220 221 #[derive(Debug)] 222 pub struct A2dpError<'a> { 223 status: u32, 224 error_code: u8, 225 error_msg: &'a CxxString, 226 } 227 228 unsafe extern "C++" { 229 include!("btav/btav_shim.h"); 230 include!("btav_sink/btav_sink_shim.h"); 231 232 type A2dpIntf; 233 type A2dpSinkIntf; 234 GetA2dpProfile(btif: *const u8) -> UniquePtr<A2dpIntf>235 unsafe fn GetA2dpProfile(btif: *const u8) -> UniquePtr<A2dpIntf>; 236 init(self: &A2dpIntf) -> i32237 fn init(self: &A2dpIntf) -> i32; connect(self: &A2dpIntf, bt_addr: RawAddress) -> u32238 fn connect(self: &A2dpIntf, bt_addr: RawAddress) -> u32; disconnect(self: &A2dpIntf, bt_addr: RawAddress) -> u32239 fn disconnect(self: &A2dpIntf, bt_addr: RawAddress) -> u32; set_silence_device(self: &A2dpIntf, bt_addr: RawAddress, silent: bool) -> i32240 fn set_silence_device(self: &A2dpIntf, bt_addr: RawAddress, silent: bool) -> i32; set_active_device(self: &A2dpIntf, bt_addr: RawAddress) -> i32241 fn set_active_device(self: &A2dpIntf, bt_addr: RawAddress) -> i32; config_codec( self: &A2dpIntf, bt_addr: RawAddress, codec_preferences: Vec<A2dpCodecConfig>, ) -> i32242 fn config_codec( 243 self: &A2dpIntf, 244 bt_addr: RawAddress, 245 codec_preferences: Vec<A2dpCodecConfig>, 246 ) -> i32; set_audio_config(self: &A2dpIntf, config: A2dpCodecConfig) -> bool247 fn set_audio_config(self: &A2dpIntf, config: A2dpCodecConfig) -> bool; start_audio_request(self: &A2dpIntf) -> bool248 fn start_audio_request(self: &A2dpIntf) -> bool; stop_audio_request(self: &A2dpIntf) -> bool249 fn stop_audio_request(self: &A2dpIntf) -> bool; suspend_audio_request(self: &A2dpIntf) -> bool250 fn suspend_audio_request(self: &A2dpIntf) -> bool; cleanup(self: &A2dpIntf)251 fn cleanup(self: &A2dpIntf); get_presentation_position(self: &A2dpIntf) -> RustPresentationPosition252 fn get_presentation_position(self: &A2dpIntf) -> RustPresentationPosition; 253 // A2dp sink functions 254 GetA2dpSinkProfile(btif: *const u8) -> UniquePtr<A2dpSinkIntf>255 unsafe fn GetA2dpSinkProfile(btif: *const u8) -> UniquePtr<A2dpSinkIntf>; 256 init(self: &A2dpSinkIntf) -> i32257 fn init(self: &A2dpSinkIntf) -> i32; connect(self: &A2dpSinkIntf, bt_addr: RawAddress) -> i32258 fn connect(self: &A2dpSinkIntf, bt_addr: RawAddress) -> i32; disconnect(self: &A2dpSinkIntf, bt_addr: RawAddress) -> i32259 fn disconnect(self: &A2dpSinkIntf, bt_addr: RawAddress) -> i32; set_active_device(self: &A2dpSinkIntf, bt_addr: RawAddress) -> i32260 fn set_active_device(self: &A2dpSinkIntf, bt_addr: RawAddress) -> i32; cleanup(self: &A2dpSinkIntf)261 fn cleanup(self: &A2dpSinkIntf); 262 } 263 extern "Rust" { connection_state_callback(addr: RawAddress, state: u32, error: A2dpError)264 fn connection_state_callback(addr: RawAddress, state: u32, error: A2dpError); audio_state_callback(addr: RawAddress, state: u32)265 fn audio_state_callback(addr: RawAddress, state: u32); audio_config_callback( addr: RawAddress, codec_config: A2dpCodecConfig, codecs_local_capabilities: &Vec<A2dpCodecConfig>, codecs_selectable_capabilities: &Vec<A2dpCodecConfig>, )266 fn audio_config_callback( 267 addr: RawAddress, 268 codec_config: A2dpCodecConfig, 269 codecs_local_capabilities: &Vec<A2dpCodecConfig>, 270 codecs_selectable_capabilities: &Vec<A2dpCodecConfig>, 271 ); mandatory_codec_preferred_callback(addr: RawAddress)272 fn mandatory_codec_preferred_callback(addr: RawAddress); 273 274 // Currently only by qualification tests. sink_audio_config_callback(addr: RawAddress, sample_rate: u32, channel_count: u8)275 fn sink_audio_config_callback(addr: RawAddress, sample_rate: u32, channel_count: u8); sink_connection_state_callback(addr: RawAddress, state: u32, error: A2dpError)276 fn sink_connection_state_callback(addr: RawAddress, state: u32, error: A2dpError); sink_audio_state_callback(addr: RawAddress, state: u32)277 fn sink_audio_state_callback(addr: RawAddress, state: u32); 278 } 279 } 280 281 pub type A2dpCodecConfig = ffi::A2dpCodecConfig; 282 pub type PresentationPosition = ffi::RustPresentationPosition; 283 pub type FfiA2dpError<'a> = ffi::A2dpError<'a>; 284 285 impl Default for A2dpCodecConfig { default() -> A2dpCodecConfig286 fn default() -> A2dpCodecConfig { 287 A2dpCodecConfig { 288 codec_type: 0, 289 codec_priority: 0, 290 sample_rate: 0, 291 bits_per_sample: 0, 292 channel_mode: 0, 293 codec_specific_1: 0, 294 codec_specific_2: 0, 295 codec_specific_3: 0, 296 codec_specific_4: 0, 297 } 298 } 299 } 300 301 impl<'a> Into<A2dpError> for FfiA2dpError<'a> { into(self) -> A2dpError302 fn into(self) -> A2dpError { 303 A2dpError { 304 status: self.status.into(), 305 error: self.error_code as i32, 306 error_message: if self.error_msg == "" { 307 None 308 } else { 309 Some(self.error_msg.to_string()) 310 }, 311 } 312 } 313 } 314 315 #[derive(Debug)] 316 pub enum A2dpCallbacks { 317 ConnectionState(RawAddress, BtavConnectionState, A2dpError), 318 AudioState(RawAddress, BtavAudioState), 319 AudioConfig(RawAddress, A2dpCodecConfig, Vec<A2dpCodecConfig>, Vec<A2dpCodecConfig>), 320 MandatoryCodecPreferred(RawAddress), 321 } 322 323 pub struct A2dpCallbacksDispatcher { 324 pub dispatch: Box<dyn Fn(A2dpCallbacks) + Send>, 325 } 326 327 impl Debug for A2dpCallbacksDispatcher { fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error>328 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { 329 write!(f, "A2dpCallbacksDispatcher {{}}") 330 } 331 } 332 333 type A2dpCb = Arc<Mutex<A2dpCallbacksDispatcher>>; 334 335 cb_variant!(A2dpCb, connection_state_callback -> A2dpCallbacks::ConnectionState, 336 RawAddress, u32 -> BtavConnectionState, FfiA2dpError -> A2dpError,{ 337 let _2 = _2.into(); 338 }); 339 340 cb_variant!(A2dpCb, audio_state_callback -> A2dpCallbacks::AudioState, RawAddress, u32 -> BtavAudioState); 341 342 cb_variant!(A2dpCb, mandatory_codec_preferred_callback -> A2dpCallbacks::MandatoryCodecPreferred, RawAddress); 343 344 cb_variant!(A2dpCb, audio_config_callback -> A2dpCallbacks::AudioConfig, 345 RawAddress, A2dpCodecConfig, &Vec<A2dpCodecConfig>, &Vec<A2dpCodecConfig>, { 346 let _2: Vec<A2dpCodecConfig> = _2.to_vec(); 347 let _3: Vec<A2dpCodecConfig> = _3.to_vec(); 348 }); 349 350 pub struct A2dp { 351 internal: cxx::UniquePtr<ffi::A2dpIntf>, 352 _is_init: bool, 353 _is_enabled: bool, 354 } 355 356 // For *const u8 opaque btif 357 unsafe impl Send for A2dp {} 358 359 impl ToggleableProfile for A2dp { is_enabled(&self) -> bool360 fn is_enabled(&self) -> bool { 361 self._is_enabled 362 } 363 enable(&mut self) -> bool364 fn enable(&mut self) -> bool { 365 self.internal.init(); 366 self._is_enabled = true; 367 true 368 } 369 370 #[profile_enabled_or(false)] disable(&mut self) -> bool371 fn disable(&mut self) -> bool { 372 self.internal.cleanup(); 373 self._is_enabled = false; 374 true 375 } 376 } 377 378 impl A2dp { 379 #[log_args] new(intf: &BluetoothInterface) -> A2dp380 pub fn new(intf: &BluetoothInterface) -> A2dp { 381 let a2dpif: cxx::UniquePtr<ffi::A2dpIntf>; 382 unsafe { 383 a2dpif = ffi::GetA2dpProfile(intf.as_raw_ptr()); 384 } 385 386 A2dp { internal: a2dpif, _is_init: false, _is_enabled: false } 387 } 388 389 #[log_args] is_initialized(&self) -> bool390 pub fn is_initialized(&self) -> bool { 391 self._is_init 392 } 393 394 #[log_args] initialize(&mut self, callbacks: A2dpCallbacksDispatcher) -> bool395 pub fn initialize(&mut self, callbacks: A2dpCallbacksDispatcher) -> bool { 396 if get_dispatchers().lock().unwrap().set::<A2dpCb>(Arc::new(Mutex::new(callbacks))) { 397 panic!("Tried to set dispatcher for A2dp callbacks while it already exists"); 398 } 399 400 if self._is_init { 401 warn!("A2dp has already been initialized"); 402 return false; 403 } 404 405 true 406 } 407 408 #[log_args] 409 #[profile_enabled_or(BtStatus::NotReady)] connect(&mut self, addr: RawAddress) -> BtStatus410 pub fn connect(&mut self, addr: RawAddress) -> BtStatus { 411 BtStatus::from(self.internal.connect(addr)) 412 } 413 414 #[log_args] 415 #[profile_enabled_or] set_active_device(&mut self, addr: RawAddress)416 pub fn set_active_device(&mut self, addr: RawAddress) { 417 self.internal.set_active_device(addr); 418 } 419 420 #[log_args] 421 #[profile_enabled_or(BtStatus::NotReady)] disconnect(&mut self, addr: RawAddress) -> BtStatus422 pub fn disconnect(&mut self, addr: RawAddress) -> BtStatus { 423 BtStatus::from(self.internal.disconnect(addr)) 424 } 425 426 #[log_args] 427 #[profile_enabled_or] config_codec(&self, addr: RawAddress, config: Vec<A2dpCodecConfig>)428 pub fn config_codec(&self, addr: RawAddress, config: Vec<A2dpCodecConfig>) { 429 self.internal.config_codec(addr, config); 430 } 431 432 #[log_args] 433 #[profile_enabled_or(false)] start_audio_request(&self) -> bool434 pub fn start_audio_request(&self) -> bool { 435 self.internal.start_audio_request() 436 } 437 438 #[log_args] 439 #[profile_enabled_or] stop_audio_request(&self)440 pub fn stop_audio_request(&self) { 441 self.internal.stop_audio_request(); 442 } 443 444 #[log_args] 445 #[profile_enabled_or] suspend_audio_request(&self)446 pub fn suspend_audio_request(&self) { 447 self.internal.suspend_audio_request(); 448 } 449 450 #[log_args] 451 #[profile_enabled_or_default] get_presentation_position(&self) -> PresentationPosition452 pub fn get_presentation_position(&self) -> PresentationPosition { 453 self.internal.get_presentation_position() 454 } 455 } 456 457 #[derive(Debug)] 458 pub enum A2dpSinkCallbacks { 459 ConnectionState(RawAddress, BtavConnectionState, A2dpError), 460 AudioState(RawAddress, BtavAudioState), 461 AudioConfig(RawAddress, u32, u8), 462 } 463 464 pub struct A2dpSinkCallbacksDispatcher { 465 pub dispatch: Box<dyn Fn(A2dpSinkCallbacks) + Send>, 466 } 467 468 impl Debug for A2dpSinkCallbacksDispatcher { fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error>469 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { 470 write!(f, "A2dpSinkCallbacksDispatcher {{}}") 471 } 472 } 473 474 type A2dpSinkCb = Arc<Mutex<A2dpSinkCallbacksDispatcher>>; 475 476 cb_variant!(A2dpSinkCb, sink_connection_state_callback -> A2dpSinkCallbacks::ConnectionState, 477 RawAddress, u32 -> BtavConnectionState, FfiA2dpError -> A2dpError,{ 478 let _2 = _2.into(); 479 }); 480 481 cb_variant!(A2dpSinkCb, sink_audio_state_callback -> A2dpSinkCallbacks::AudioState, RawAddress, u32 -> BtavAudioState); 482 483 cb_variant!(A2dpSinkCb, sink_audio_config_callback -> A2dpSinkCallbacks::AudioConfig, RawAddress, u32, u8); 484 485 pub struct A2dpSink { 486 internal: cxx::UniquePtr<ffi::A2dpSinkIntf>, 487 _is_init: bool, 488 _is_enabled: bool, 489 } 490 491 // For *const u8 opaque btif 492 unsafe impl Send for A2dpSink {} 493 494 impl ToggleableProfile for A2dpSink { is_enabled(&self) -> bool495 fn is_enabled(&self) -> bool { 496 self._is_enabled 497 } 498 enable(&mut self) -> bool499 fn enable(&mut self) -> bool { 500 self.internal.init(); 501 self._is_enabled = true; 502 true 503 } 504 505 #[profile_enabled_or(false)] disable(&mut self) -> bool506 fn disable(&mut self) -> bool { 507 self.internal.cleanup(); 508 self._is_enabled = false; 509 true 510 } 511 } 512 513 impl A2dpSink { 514 #[log_args] new(intf: &BluetoothInterface) -> A2dpSink515 pub fn new(intf: &BluetoothInterface) -> A2dpSink { 516 let a2dp_sink: cxx::UniquePtr<ffi::A2dpSinkIntf>; 517 unsafe { 518 a2dp_sink = ffi::GetA2dpSinkProfile(intf.as_raw_ptr()); 519 } 520 521 A2dpSink { internal: a2dp_sink, _is_init: false, _is_enabled: false } 522 } 523 524 #[log_args] is_initialized(&self) -> bool525 pub fn is_initialized(&self) -> bool { 526 self._is_init 527 } 528 529 #[log_args] initialize(&mut self, callbacks: A2dpSinkCallbacksDispatcher) -> bool530 pub fn initialize(&mut self, callbacks: A2dpSinkCallbacksDispatcher) -> bool { 531 if get_dispatchers().lock().unwrap().set::<A2dpSinkCb>(Arc::new(Mutex::new(callbacks))) { 532 panic!("Tried to set dispatcher for A2dp Sink Callbacks while it already exists"); 533 } 534 self._is_init = true; 535 true 536 } 537 538 #[log_args] 539 #[profile_enabled_or] connect(&mut self, bt_addr: RawAddress)540 pub fn connect(&mut self, bt_addr: RawAddress) { 541 self.internal.connect(bt_addr); 542 } 543 544 #[log_args] 545 #[profile_enabled_or] disconnect(&mut self, bt_addr: RawAddress)546 pub fn disconnect(&mut self, bt_addr: RawAddress) { 547 self.internal.disconnect(bt_addr); 548 } 549 550 #[log_args] 551 #[profile_enabled_or] set_active_device(&mut self, bt_addr: RawAddress)552 pub fn set_active_device(&mut self, bt_addr: RawAddress) { 553 self.internal.set_active_device(bt_addr); 554 } 555 556 #[log_args] 557 #[profile_enabled_or] cleanup(&mut self)558 pub fn cleanup(&mut self) {} 559 } 560 561 #[cfg(test)] 562 mod tests { 563 use super::*; 564 565 #[test] validate_sample_rate()566 fn validate_sample_rate() { 567 assert!(!A2dpCodecSampleRate::validate_bits(256)); 568 assert!(A2dpCodecSampleRate::validate_bits(2 + 32 + 128)); 569 } 570 571 #[test] validate_bits_per_sample()572 fn validate_bits_per_sample() { 573 assert!(!A2dpCodecBitsPerSample::validate_bits(8)); 574 assert!(A2dpCodecBitsPerSample::validate_bits(1 + 4)); 575 } 576 577 #[test] validate_channel_mode()578 fn validate_channel_mode() { 579 assert!(!A2dpCodecChannelMode::validate_bits(4)); 580 assert!(A2dpCodecChannelMode::validate_bits(1 + 2)); 581 } 582 } 583