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 socket manager interface."""
15
16import logging
17
18from floss.pandora.floss import floss_enums
19from floss.pandora.floss import observer_base
20from floss.pandora.floss import utils
21from gi.repository import GLib
22
23
24class SocketManagerCallbacks:
25    """Callbacks for the socket manager interface.
26
27    Implement this to observe these callbacks when exporting callbacks via register_callback.
28    """
29
30    def on_incoming_socket_ready(self, socket, status):
31        """Called when incoming socket is ready.
32
33        Args:
34            socket: BluetoothServerSocket.
35            status: BtStatus.
36        """
37        pass
38
39    def on_incoming_socket_closed(self, listener_id, reason):
40        """Called when incoming socket is closed.
41
42        Args:
43            listener_id: SocketId.
44            reason: BtStatus.
45        """
46        pass
47
48    def on_handle_incoming_connection(
49            self,
50            listener_id,
51            connection,
52            *,  # Keyword only after bare asterisk
53            dbus_unix_fd_list=None):
54        """Called when incoming connection is handled.
55
56        Args:
57            listener_id: SocketId.
58            connection: BluetoothSocket.
59            dbus_unix_fd_list: List of fds. Use the handle inside connection to look up the target fd. If it would be
60                               kept, dup it with os.dup first. CrOS specific pydbus feature.
61        """
62        pass
63
64    def on_outgoing_connection_result(
65            self,
66            connecting_id,
67            result,
68            socket,
69            *,  # Keyword only after bare asterisk
70            dbus_unix_fd_list=None):
71        """Called when outgoing connection is handled.
72
73        Args:
74            connecting_id: SocketId.
75            result: BtStatus.
76            socket: BluetoothSocket.
77            dbus_unix_fd_list: List of fds. Use the handle inside socket to look up the target fd. If it would be kept,
78                               dup it with os.dup first. CrOS specific pydbus feature.
79        """
80        pass
81
82
83class FlossSocketManagerClient(SocketManagerCallbacks):
84    """Handles method calls and callbacks from the socket manager interface."""
85
86    ADAPTER_SERVICE = 'org.chromium.bluetooth'
87    SOCKET_MANAGER_INTERFACE = 'org.chromium.bluetooth.SocketManager'
88    ADAPTER_OBJECT_PATTERN = '/org/chromium/bluetooth/hci{}/adapter'
89    SOCKET_CB_OBJ_NAME = 'test_socket_client'
90    CB_EXPORTED_INTF = 'org.chromium.bluetooth.SocketManagerCallback'
91    FLOSS_RESPONSE_LATENCY_SECS = 3
92
93    class ExportedSocketManagerCallbacks(observer_base.ObserverBase):
94        """
95        <node>
96            <interface name="org.chromium.bluetooth.SocketManagerCallback">
97                <method name="OnIncomingSocketReady">
98                    <arg type="a{sv}" name="socket" direction="in" />
99                    <arg type="u" name="status" direction="in" />
100                </method>
101                <method name="OnIncomingSocketClosed">
102                    <arg type="t" name="listener_id" direction="in" />
103                    <arg type="u" name="reason" direction="in" />
104                </method>
105                <method name="OnHandleIncomingConnection">
106                    <arg type="t" name="listener_id" direction="in" />
107                    <arg type="a{sv}" name="connection" direction="in" />
108                </method>
109                <method name="OnOutgoingConnectionResult">
110                    <arg type="t" name="connecting_id" direction="in" />
111                    <arg type="u" name="result" direction="in" />
112                    <arg type="a{sv}" name="socket" direction="in" />
113                </method>
114            </interface>
115        </node>
116        """
117
118        def __init__(self):
119            """Constructs exported callbacks object."""
120            observer_base.ObserverBase.__init__(self)
121
122        def OnIncomingSocketReady(self, socket, status):
123            """Handles incoming socket ready callback."""
124            for observer in self.observers.values():
125                observer.on_incoming_socket_ready(socket, status)
126
127        def OnIncomingSocketClosed(self, listener_id, reason):
128            """Handles incoming socket closed callback."""
129            for observer in self.observers.values():
130                observer.on_incoming_socket_closed(listener_id, reason)
131
132        def OnHandleIncomingConnection(self, listener_id, connection, *, dbus_unix_fd_list=None):
133            """Handles incoming socket connection callback."""
134            for observer in self.observers.values():
135                observer.on_handle_incoming_connection(listener_id, connection, dbus_unix_fd_list=dbus_unix_fd_list)
136
137        def OnOutgoingConnectionResult(self, connecting_id, result, socket, *, dbus_unix_fd_list=None):
138            """Handles outgoing socket connection callback."""
139            for observer in self.observers.values():
140                observer.on_outgoing_connection_result(connecting_id,
141                                                       result,
142                                                       socket,
143                                                       dbus_unix_fd_list=dbus_unix_fd_list)
144
145    def __init__(self, bus, hci):
146        self.bus = bus
147        self.hci = hci
148        self.callbacks = None
149        self.callback_id = None
150        self.objpath = self.ADAPTER_OBJECT_PATTERN.format(hci)
151
152    def __del__(self):
153        """Destructor."""
154        del self.callbacks
155
156    @utils.glib_callback()
157    def on_incoming_socket_ready(self, socket, status):
158        """Handles incoming socket ready callback."""
159        logging.debug('on_incoming_socket_ready: socket: %s, status: %s', socket, status)
160
161    @utils.glib_callback()
162    def on_incoming_socket_closed(self, listener_id, reason):
163        """Handles incoming socket closed callback."""
164        logging.debug('on_incoming_socket_closed: listener_id: %s, reason: %s', listener_id, reason)
165
166    @utils.glib_callback()
167    def on_handle_incoming_connection(self, listener_id, connection, *, dbus_unix_fd_list=None):
168        """Handles incoming socket connection callback."""
169        logging.debug('on_handle_incoming_connection: listener_id: %s, connection: %s', listener_id, connection)
170
171    @utils.glib_callback()
172    def on_outgoing_connection_result(self, connecting_id, result, socket, *, dbus_unix_fd_list=None):
173        """Handles outgoing socket connection callback."""
174        logging.debug('on_outgoing_connection_result: connecting_id: %s, result: %s, socket: %s', connecting_id, result,
175                      socket)
176
177    def make_dbus_device(self, address, name):
178        return {'address': GLib.Variant('s', address), 'name': GLib.Variant('s', name)}
179
180    def _make_dbus_timeout(self, timeout):
181        return utils.dbus_optional_value('u', timeout)
182
183    @utils.glib_call(False)
184    def has_proxy(self):
185        """Checks whether manager proxy can be acquired."""
186        return bool(self.proxy())
187
188    def proxy(self):
189        """Gets proxy object to socket manager interface for method calls."""
190        return self.bus.get(self.ADAPTER_SERVICE, self.objpath)[self.SOCKET_MANAGER_INTERFACE]
191
192    @utils.glib_call(False)
193    def register_callbacks(self):
194        """Registers socket manager callbacks if one doesn't already exist.
195
196        Returns:
197            True on success, False otherwise.
198        """
199        # Callbacks already registered
200        if self.callbacks:
201            return True
202
203        # Create and publish callbacks
204        self.callbacks = self.ExportedSocketManagerCallbacks()
205        self.callbacks.add_observer('socket_client', self)
206        objpath = utils.generate_dbus_cb_objpath(self.SOCKET_CB_OBJ_NAME, self.hci)
207        self.bus.register_object(objpath, self.callbacks, None)
208
209        # Register published callbacks with adapter daemon
210        self.callback_id = self.proxy().RegisterCallback(objpath)
211        return True
212
213    def register_callback_observer(self, name, observer):
214        """Add an observer for all callbacks.
215
216        Args:
217            name: Name of the observer.
218            observer: Observer that implements all callback classes.
219        """
220        if isinstance(observer, SocketManagerCallbacks):
221            self.callbacks.add_observer(name, observer)
222
223    def unregister_callback_observer(self, name, observer):
224        """Remove an observer for all callbacks.
225
226        Args:
227            name: Name of the observer.
228            observer: Observer that implements all callback classes.
229        """
230        if isinstance(observer, SocketManagerCallbacks):
231            self.callbacks.remove_observer(name, observer)
232
233    @utils.glib_call(None)
234    def listen_using_l2cap_channel(self):
235        """Listens using L2CAP channel.
236
237        Returns:
238            SocketResult as {status:BtStatus, id:int} on success, None otherwise.
239        """
240        return self.proxy().ListenUsingL2capChannel(self.callback_id)
241
242    @utils.glib_call(None)
243    def listen_using_insecure_l2cap_channel(self):
244        """Listens using insecure L2CAP channel.
245
246        Returns:
247            SocketResult as {status:BtStatus, id:int} on success, None otherwise.
248        """
249
250        return self.proxy().ListenUsingInsecureL2capChannel(self.callback_id)
251
252    @utils.glib_call(None)
253    def listen_using_insecure_rfcomm_with_service_record(self, name, uuid):
254        """Listens using insecure RFCOMM channel with service record.
255
256        Args:
257            name: Service name.
258            uuid: 128-bit service UUID.
259
260        Returns:
261            SocketResult as {status:BtStatus, id:int} on success, None otherwise.
262        """
263        return self.proxy().ListenUsingInsecureRfcommWithServiceRecord(self.callback_id, name, uuid)
264
265    @utils.glib_call(None)
266    def listen_using_rfcomm_with_service_record(self, name, uuid):
267        """Listens using RFCOMM channel with service record.
268
269        Args:
270            name: Service name.
271            uuid: 128-bit service UUID.
272
273        Returns:
274            SocketResult as {status:BtStatus, id:int} on success, None otherwise.
275        """
276        return self.proxy().ListenUsingRfcommWithServiceRecord(self.callback_id, name, uuid)
277
278    @utils.glib_call(None)
279    def create_insecure_l2cap_channel(self, device, psm):
280        """Creates insecure L2CAP channel.
281
282        Args:
283            device: D-bus device.
284            psm: Protocol Service Multiplexor.
285
286        Returns:
287            SocketResult as {status:BtStatus, id:int} on success, None otherwise.
288        """
289        return self.proxy().CreateInsecureL2capChannel(self.callback_id, device, psm)
290
291    @utils.glib_call(None)
292    def create_l2cap_channel(self, device, psm):
293        """Creates L2CAP channel.
294
295        Args:
296            device: D-bus device.
297            psm: Protocol Service Multiplexor.
298
299        Returns:
300            SocketResult as {status:BtStatus, id:int} on success, None otherwise.
301        """
302        return self.proxy().CreateL2capChannel(self.callback_id, device, psm)
303
304    @utils.glib_call(None)
305    def create_insecure_rfcomm_socket_to_service_record(self, device, uuid):
306        """Creates insecure RFCOMM socket to service record.
307
308        Args:
309            device: New D-bus device.
310            uuid: 128-bit service UUID.
311
312        Returns:
313            SocketResult as {status:BtStatus, id:int} on success, None otherwise.
314        """
315        return self.proxy().CreateInsecureRfcommSocketToServiceRecord(self.callback_id, device, uuid)
316
317    @utils.glib_call(None)
318    def create_rfcomm_socket_to_service_record(self, device, uuid):
319        """Creates RFCOMM socket to service record.
320
321        Args:
322            device: D-bus device.
323            uuid: 128-bit service UUID.
324
325        Returns:
326            SocketResult as {status:BtStatus, id:int} on success, None otherwise.
327        """
328        return self.proxy().CreateRfcommSocketToServiceRecord(self.callback_id, device, uuid)
329
330    @utils.glib_call(None)
331    def accept(self, socket_id, timeout_ms=None):
332        """Accepts socket connection.
333
334        Args:
335            socket_id: New address of the adapter.
336            timeout_ms: Timeout in ms.
337
338        Returns:
339            BtStatus as int on success, None otherwise.
340        """
341        timeout_ms = self._make_dbus_timeout(timeout_ms)
342        return self.proxy().Accept(self.callback_id, socket_id, timeout_ms)
343
344    @utils.glib_call(False)
345    def close(self, socket_id):
346        """Closes socket connection.
347
348        Args:
349            socket_id: Socket id to be closed.
350
351        Returns:
352            True on success, False otherwise.
353        """
354        status = self.proxy().Close(self.callback_id, socket_id)
355        if floss_enums.BtStatus(status) != floss_enums.BtStatus.SUCCESS:
356            logging.error('Failed to close socket with id: %s, status = %s', socket_id, status)
357        return floss_enums.BtStatus(status) == floss_enums.BtStatus.SUCCESS
358