1# Copyright 2024 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""Client class to access the Floss media interface.""" 15 16import logging 17 18from floss.pandora.floss import observer_base 19from floss.pandora.floss import utils 20from gi.repository import GLib 21 22 23class BluetoothMediaCallbacks: 24 """Callbacks for the media interface. 25 26 Implement this to observe these callbacks when exporting callbacks via register_callback. 27 """ 28 29 def on_bluetooth_audio_device_added(self, device): 30 """Called when a Bluetooth audio device is added. 31 32 Args: 33 device: The struct of BluetoothAudioDevice. 34 """ 35 pass 36 37 def on_bluetooth_audio_device_removed(self, addr): 38 """Called when a Bluetooth audio device is removed. 39 40 Args: 41 addr: Address of device to be removed. 42 """ 43 pass 44 45 def on_absolute_volume_supported_changed(self, supported): 46 """Called when the support of using absolute volume is changed. 47 48 Args: 49 supported: The boolean value indicates whether the supported volume has changed. 50 """ 51 pass 52 53 def on_absolute_volume_changed(self, volume): 54 """Called when the absolute volume is changed. 55 56 Args: 57 volume: The value of volume. 58 """ 59 pass 60 61 def on_hfp_volume_changed(self, volume, addr): 62 """Called when the HFP volume is changed. 63 64 Args: 65 volume: The value of volume. 66 addr: Device address to get the HFP volume. 67 """ 68 pass 69 70 def on_hfp_audio_disconnected(self, addr): 71 """Called when the HFP audio is disconnected. 72 73 Args: 74 addr: Device address to get the HFP state. 75 """ 76 pass 77 78 79class FlossMediaClient(BluetoothMediaCallbacks): 80 """Handles method calls to and callbacks from the media interface.""" 81 82 MEDIA_SERVICE = 'org.chromium.bluetooth' 83 MEDIA_INTERFACE = 'org.chromium.bluetooth.BluetoothMedia' 84 MEDIA_OBJECT_PATTERN = '/org/chromium/bluetooth/hci{}/media' 85 86 MEDIA_CB_INTF = 'org.chromium.bluetooth.BluetoothMediaCallback' 87 MEDIA_CB_OBJ_NAME = 'test_media_client' 88 89 class ExportedMediaCallbacks(observer_base.ObserverBase): 90 """ 91 <node> 92 <interface name="org.chromium.bluetooth.BluetoothMediaCallback"> 93 <method name="OnBluetoothAudioDeviceAdded"> 94 <arg type="a{sv}" name="device" direction="in" /> 95 </method> 96 <method name="OnBluetoothAudioDeviceRemoved"> 97 <arg type="s" name="addr" direction="in" /> 98 </method> 99 <method name="OnAbsoluteVolumeSupportedChanged"> 100 <arg type="b" name="supported" direction="in" /> 101 </method> 102 <method name="OnAbsoluteVolumeChanged"> 103 <arg type="y" name="volume" direction="in" /> 104 </method> 105 <method name="OnHfpVolumeChanged"> 106 <arg type="y" name="volume" direction="in" /> 107 <arg type="s" name="addr" direction="in" /> 108 </method> 109 <method name="OnHfpAudioDisconnected"> 110 <arg type="s" name="addr" direction="in" /> 111 </method> 112 </interface> 113 </node> 114 """ 115 116 def __init__(self): 117 """Constructs exported callbacks object.""" 118 observer_base.ObserverBase.__init__(self) 119 120 def OnBluetoothAudioDeviceAdded(self, device): 121 """Handles Bluetooth audio device added callback. 122 123 Args: 124 device: The struct of BluetoothAudioDevice. 125 """ 126 for observer in self.observers.values(): 127 observer.on_bluetooth_audio_device_added(device) 128 129 def OnBluetoothAudioDeviceRemoved(self, addr): 130 """Handles Bluetooth audio device removed callback. 131 132 Args: 133 addr: Address of device to be removed. 134 """ 135 for observer in self.observers.values(): 136 observer.on_bluetooth_audio_device_removed(addr) 137 138 def OnAbsoluteVolumeSupportedChanged(self, supported): 139 """Handles absolute volume supported changed callback. 140 141 Args: 142 supported: The boolean value indicates whether the supported volume has changed. 143 """ 144 for observer in self.observers.values(): 145 observer.on_absolute_volume_supported_changed(supported) 146 147 def OnAbsoluteVolumeChanged(self, volume): 148 """Handles absolute volume changed callback. 149 150 Args: 151 volume: The value of volume. 152 """ 153 for observer in self.observers.values(): 154 observer.on_absolute_volume_changed(volume) 155 156 def OnHfpVolumeChanged(self, volume, addr): 157 """Handles HFP volume changed callback. 158 159 Args: 160 volume: The value of volume. 161 addr: Device address to get the HFP volume. 162 """ 163 for observer in self.observers.values(): 164 observer.on_hfp_volume_changed(volume, addr) 165 166 def OnHfpAudioDisconnected(self, addr): 167 """Handles HFP audio disconnected callback. 168 169 Args: 170 addr: Device address to get the HFP state. 171 """ 172 for observer in self.observers.values(): 173 observer.on_hfp_audio_disconnected(addr) 174 175 def __init__(self, bus, hci): 176 """Constructs the client. 177 178 Args: 179 bus: D-Bus bus over which we'll establish connections. 180 hci: HCI adapter index. Get this value from 'get_default_adapter' on FlossManagerClient. 181 """ 182 self.bus = bus 183 self.hci = hci 184 self.objpath = self.MEDIA_OBJECT_PATTERN.format(hci) 185 self.devices = [] 186 187 # We don't register callbacks by default. 188 self.callbacks = None 189 190 def __del__(self): 191 """Destructor.""" 192 del self.callbacks 193 194 @utils.glib_callback() 195 def on_bluetooth_audio_device_added(self, device): 196 """Handles Bluetooth audio device added callback. 197 198 Args: 199 device: The struct of BluetoothAudioDevice. 200 """ 201 logging.debug('on_bluetooth_audio_device_added: device: %s', device) 202 if device['address'] in self.devices: 203 logging.debug("Device already added") 204 self.devices.append(device['address']) 205 206 @utils.glib_callback() 207 def on_bluetooth_audio_device_removed(self, addr): 208 """Handles Bluetooth audio device removed callback. 209 210 Args: 211 addr: Address of device to be removed. 212 """ 213 logging.debug('on_bluetooth_audio_device_removed: address: %s', addr) 214 if addr in self.devices: 215 self.devices.remove(addr) 216 217 @utils.glib_callback() 218 def on_absolute_volume_supported_changed(self, supported): 219 """Handles absolute volume supported changed callback. 220 221 Args: 222 supported: The boolean value indicates whether the supported volume has changed. 223 """ 224 logging.debug('on_absolute_volume_supported_changed: supported: %s', supported) 225 226 @utils.glib_callback() 227 def on_absolute_volume_changed(self, volume): 228 """Handles absolute volume changed callback. 229 230 Args: 231 volume: The value of volume. 232 """ 233 logging.debug('on_absolute_volume_changed: volume: %s', volume) 234 235 @utils.glib_callback() 236 def on_hfp_volume_changed(self, volume, addr): 237 """Handles HFP volume changed callback. 238 239 Args: 240 volume: The value of volume. 241 addr: Device address to get the HFP volume. 242 """ 243 logging.debug('on_hfp_volume_changed: volume: %s, address: %s', volume, addr) 244 245 @utils.glib_callback() 246 def on_hfp_audio_disconnected(self, addr): 247 """Handles HFP audio disconnected callback. 248 249 Args: 250 addr: Device address to get the HFP state. 251 """ 252 logging.debug('on_hfp_audio_disconnected: address: %s', addr) 253 254 def make_dbus_player_metadata(self, title, artist, album, length): 255 """Makes struct for player metadata D-Bus. 256 257 Args: 258 title: The title of player metadata. 259 artist: The artist of player metadata. 260 album: The album of player metadata. 261 length: The value of length metadata. 262 263 Returns: 264 Dictionary of player metadata. 265 """ 266 return { 267 'title': GLib.Variant('s', title), 268 'artist': GLib.Variant('s', artist), 269 'album': GLib.Variant('s', album), 270 'length': GLib.Variant('x', length) 271 } 272 273 @utils.glib_call(False) 274 def has_proxy(self): 275 """Checks whether the media proxy is present.""" 276 return bool(self.proxy()) 277 278 def proxy(self): 279 """Gets a proxy object to media interface for method calls.""" 280 return self.bus.get(self.MEDIA_SERVICE, self.objpath)[self.MEDIA_INTERFACE] 281 282 @utils.glib_call(None) 283 def register_callback(self): 284 """Registers a media callback if it doesn't exist. 285 286 Returns: 287 True on success, False on failure, None on DBus error. 288 """ 289 if self.callbacks: 290 return True 291 292 # Create and publish callbacks 293 self.callbacks = self.ExportedMediaCallbacks() 294 self.callbacks.add_observer('media_client', self) 295 objpath = utils.generate_dbus_cb_objpath(self.MEDIA_CB_OBJ_NAME, self.hci) 296 self.bus.register_object(objpath, self.callbacks, None) 297 298 # Register published callbacks with media daemon 299 return self.proxy().RegisterCallback(objpath) 300 301 @utils.glib_call(None) 302 def initialize(self): 303 """Initializes the media (both A2DP and AVRCP) stack. 304 305 Returns: 306 True on success, False on failure, None on DBus error. 307 """ 308 return self.proxy().Initialize() 309 310 @utils.glib_call(None) 311 def cleanup(self): 312 """Cleans up media stack. 313 314 Returns: 315 True on success, False on failure, None on DBus error. 316 """ 317 return self.proxy().Cleanup() 318 319 @utils.glib_call(False) 320 def connect(self, address): 321 """Connects to a Bluetooth media device with the specified address. 322 323 Args: 324 address: Device address to connect. 325 326 Returns: 327 True on success, False otherwise. 328 """ 329 self.proxy().Connect(address) 330 return True 331 332 @utils.glib_call(False) 333 def disconnect(self, address): 334 """Disconnects the specified Bluetooth media device. 335 336 Args: 337 address: Device address to disconnect. 338 339 Returns: 340 True on success, False otherwise. 341 """ 342 self.proxy().Disconnect(address) 343 return True 344 345 @utils.glib_call(False) 346 def set_active_device(self, address): 347 """Sets the device as the active A2DP device. 348 349 Args: 350 address: Device address to set as an active A2DP device. 351 352 Returns: 353 True on success, False otherwise. 354 """ 355 self.proxy().SetActiveDevice(address) 356 return True 357 358 @utils.glib_call(False) 359 def set_hfp_active_device(self, address): 360 """Sets the device as the active HFP device. 361 362 Args: 363 address: Device address to set as an active HFP device. 364 365 Returns: 366 True on success, False otherwise. 367 """ 368 self.proxy().SetHfpActiveDevice(address) 369 return True 370 371 @utils.glib_call(None) 372 def set_audio_config(self, sample_rate, bits_per_sample, channel_mode): 373 """Sets audio configuration. 374 375 Args: 376 sample_rate: Value of sample rate. 377 bits_per_sample: Number of bits per sample. 378 channel_mode: Value of channel mode. 379 380 Returns: 381 True on success, False on failure, None on DBus error. 382 """ 383 return self.proxy().SetAudioConfig(sample_rate, bits_per_sample, channel_mode) 384 385 @utils.glib_call(False) 386 def set_volume(self, volume): 387 """Sets the A2DP/AVRCP volume. 388 389 Args: 390 volume: The value of volume to set it. 391 392 Returns: 393 True on success, False otherwise. 394 """ 395 self.proxy().SetVolume(volume) 396 return True 397 398 @utils.glib_call(False) 399 def set_hfp_volume(self, volume, address): 400 """Sets the HFP speaker volume. 401 402 Args: 403 volume: The value of volume. 404 address: Device address to set the HFP volume. 405 406 Returns: 407 True on success, False otherwise. 408 """ 409 self.proxy().SetHfpVolume(volume, address) 410 return True 411 412 @utils.glib_call(False) 413 def start_audio_request(self): 414 """Starts audio request. 415 416 Returns: 417 True on success, False otherwise. 418 """ 419 self.proxy().StartAudioRequest() 420 return True 421 422 @utils.glib_call(None) 423 def get_a2dp_audio_started(self, address): 424 """Gets A2DP audio started. 425 426 Args: 427 address: Device address to get the A2DP state. 428 429 Returns: 430 Non-zero value iff A2DP audio has started, None on D-Bus error. 431 """ 432 return self.proxy().GetA2dpAudioStarted(address) 433 434 @utils.glib_call(False) 435 def stop_audio_request(self): 436 """Stops audio request. 437 438 Returns: 439 True on success, False otherwise. 440 """ 441 self.proxy().StopAudioRequest() 442 return True 443 444 @utils.glib_call(False) 445 def start_sco_call(self, address, sco_offload, force_cvsd): 446 """Starts the SCO call. 447 448 Args: 449 address: Device address to make SCO call. 450 sco_offload: Whether SCO offload is enabled. 451 force_cvsd: True to force the stack to use CVSD even if mSBC is supported. 452 453 Returns: 454 True on success, False otherwise. 455 """ 456 self.proxy().StartScoCall(address, sco_offload, force_cvsd) 457 return True 458 459 @utils.glib_call(None) 460 def get_hfp_audio_started(self, address): 461 """Gets HFP audio started. 462 463 Args: 464 address: Device address to get the HFP state. 465 466 Returns: 467 The negotiated codec (CVSD=1, mSBC=2) to use if HFP audio has started; 0 if HFP audio hasn't started, 468 None on DBus error. 469 """ 470 return self.proxy().GetHfpAudioStarted(address) 471 472 @utils.glib_call(False) 473 def stop_sco_call(self, address): 474 """Stops the SCO call. 475 476 Args: 477 address: Device address to stop SCO call. 478 479 Returns: 480 True on success, False otherwise. 481 """ 482 self.proxy().StopScoCall(address) 483 return True 484 485 @utils.glib_call(None) 486 def get_presentation_position(self): 487 """Gets presentation position. 488 489 Returns: 490 PresentationPosition struct on success, None otherwise. 491 """ 492 return self.proxy().GetPresentationPosition() 493 494 @utils.glib_call(False) 495 def set_player_position(self, position_us): 496 """Sets player position. 497 498 Args: 499 position_us: The player position in microsecond. 500 501 Returns: 502 True on success, False otherwise. 503 """ 504 self.proxy().SetPlayerPosition(position_us) 505 return True 506 507 @utils.glib_call(False) 508 def set_player_playback_status(self, status): 509 """Sets player playback status. 510 511 Args: 512 status: Playback status such as 'playing', 'paused', 'stopped' as string. 513 514 Returns: 515 True on success, False otherwise. 516 """ 517 self.proxy().SetPlayerPlaybackStatus(status) 518 return True 519 520 @utils.glib_call(False) 521 def set_player_metadata(self, metadata): 522 """Sets player metadata. 523 524 Args: 525 metadata: The media metadata to set it. 526 527 Returns: 528 True on success, False otherwise. 529 """ 530 self.proxy().SetPlayerMetadata(metadata) 531 return True 532 533 def register_callback_observer(self, name, observer): 534 """Adds an observer for all callbacks. 535 536 Args: 537 name: Name of the observer. 538 observer: Observer that implements all callback classes. 539 """ 540 if isinstance(observer, BluetoothMediaCallbacks): 541 self.callbacks.add_observer(name, observer) 542 543 def unregister_callback_observer(self, name, observer): 544 """Removes an observer for all callbacks. 545 546 Args: 547 name: Name of the observer. 548 observer: Observer that implements all callback classes. 549 """ 550 if isinstance(observer, BluetoothMediaCallbacks): 551 self.callbacks.remove_observer(name, observer) 552