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