• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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, connection_listener):
414        """Starts audio request.
415
416        Args:
417            connection_listener: The file descriptor to write 1 (u8) on audio connection.
418
419        Returns:
420            True on success, False otherwise.
421        """
422        self.proxy().StartAudioRequest(connection_listener)
423        return True
424
425    @utils.glib_call(None)
426    def get_a2dp_audio_started(self, address):
427        """Gets A2DP audio started.
428
429        Args:
430            address: Device address to get the A2DP state.
431
432        Returns:
433            Non-zero value iff A2DP audio has started, None on D-Bus error.
434        """
435        return self.proxy().GetA2dpAudioStarted(address)
436
437    @utils.glib_call(False)
438    def stop_audio_request(self, connection_listener):
439        """Stops audio request.
440
441        Args:
442            connection_listener: The file descriptor to write 0 (u8) on audio connection.
443
444        Returns:
445            True on success, False otherwise.
446        """
447        self.proxy().StopAudioRequest(connection_listener)
448        return True
449
450    @utils.glib_call(False)
451    def start_sco_call(self, address, sco_offload, disabled_codecs, connection_listener):
452        """Starts the SCO call.
453
454        Args:
455            address: Device address to make SCO call.
456            sco_offload: Whether SCO offload is enabled.
457            disabled_codecs: The disabled codecs in bitmask form. CVSD=1, MSBC=2, LC3=4.
458            connection_listener: The file descriptor to write the codec id on audio connection.
459
460        Returns:
461            True on success, False otherwise.
462        """
463        self.proxy().StartScoCall(address, sco_offload, disabled_codecs, connection_listener)
464        return True
465
466    @utils.glib_call(None)
467    def get_hfp_audio_started(self, address):
468        """Gets HFP audio started.
469
470        Args:
471            address: Device address to get the HFP state.
472
473        Returns:
474            The negotiated codec (CVSD=1, mSBC=2) to use if HFP audio has started; 0 if HFP audio hasn't started,
475            None on DBus error.
476        """
477        return self.proxy().GetHfpAudioStarted(address)
478
479    @utils.glib_call(False)
480    def stop_sco_call(self, address, connection_listener):
481        """Stops the SCO call.
482
483        Args:
484            address: Device address to stop SCO call.
485            connection_listener: The file descriptor to write 0 (u8) on audio disconnection.
486
487        Returns:
488            True on success, False otherwise.
489        """
490        self.proxy().StopScoCall(address, connection_listener)
491        return True
492
493    @utils.glib_call(None)
494    def get_presentation_position(self):
495        """Gets presentation position.
496
497        Returns:
498            PresentationPosition struct on success, None otherwise.
499        """
500        return self.proxy().GetPresentationPosition()
501
502    @utils.glib_call(False)
503    def set_player_position(self, position_us):
504        """Sets player position.
505
506        Args:
507            position_us: The player position in microsecond.
508
509        Returns:
510            True on success, False otherwise.
511        """
512        self.proxy().SetPlayerPosition(position_us)
513        return True
514
515    @utils.glib_call(False)
516    def set_player_playback_status(self, status):
517        """Sets player playback status.
518
519        Args:
520            status: Playback status such as 'playing', 'paused', 'stopped' as string.
521
522        Returns:
523            True on success, False otherwise.
524        """
525        self.proxy().SetPlayerPlaybackStatus(status)
526        return True
527
528    @utils.glib_call(False)
529    def set_player_metadata(self, metadata):
530        """Sets player metadata.
531
532        Args:
533            metadata: The media metadata to set it.
534
535        Returns:
536            True on success, False otherwise.
537        """
538        self.proxy().SetPlayerMetadata(metadata)
539        return True
540
541    def register_callback_observer(self, name, observer):
542        """Adds an observer for all callbacks.
543
544        Args:
545            name: Name of the observer.
546            observer: Observer that implements all callback classes.
547        """
548        if isinstance(observer, BluetoothMediaCallbacks):
549            self.callbacks.add_observer(name, observer)
550
551    def unregister_callback_observer(self, name, observer):
552        """Removes an observer for all callbacks.
553
554        Args:
555            name: Name of the observer.
556            observer: Observer that implements all callback classes.
557        """
558        if isinstance(observer, BluetoothMediaCallbacks):
559            self.callbacks.remove_observer(name, observer)
560