1# Copyright 2023 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"""All floss utils functions."""
15
16import functools
17import logging
18import threading
19import time
20import uuid
21from typing import List, Optional
22import traceback
23
24from floss.pandora.floss import floss_enums
25from gi.repository import GLib
26from google.protobuf import any_pb2
27from pandora import host_pb2
28from pandora_experimental import os_pb2
29
30# All GLIB method calls should wait this many seconds by default
31GLIB_METHOD_CALL_TIMEOUT = 2
32
33# GLib thread name that will run the mainloop.
34GLIB_THREAD_NAME = 'glib'
35
36
37def poll_for_condition(condition, exception=None, timeout=10, sleep_interval=0.1, desc=None):
38    """Polls until a condition is evaluated to true.
39
40    Args:
41        condition:
42            function taking no args and returning anything that will
43            evaluate to True in a conditional check.
44        exception:
45            exception to throw if condition doesn't evaluate to true.
46        timeout:
47             maximum number of seconds to wait.
48        sleep_interval:
49            time to sleep between polls.
50        desc:
51            description of default TimeoutError used if 'exception' is
52            None.
53
54    Raises:
55        TimeoutError: 'exception' arg if supplied; TimeoutError otherwise
56
57    Returns:
58        The evaluated value that caused the poll loop to terminate.
59    """
60    start_time = time.time()
61    while True:
62        value = condition()
63        if value:
64            return value
65        if time.time() + sleep_interval - start_time > timeout:
66            if exception:
67                logging.error('Will raise error %r due to unexpected return: %r', exception, value)
68                raise exception  # pylint: disable=raising-bad-type
69
70            if desc:
71                desc = 'Timed out waiting for condition: ' + desc
72            else:
73                desc = 'Timed out waiting for unnamed condition'
74            logging.error(desc)
75            raise TimeoutError()
76
77        # TODO: b/292696514 - Use event base system and remove this.
78        time.sleep(sleep_interval)
79
80
81def dbus_safe(default_return_value, return_error=False):
82    """Catches all DBus exceptions and return a default value instead.
83
84    Wrap a function with a try block that catches DBus exceptions and returns the error with the specified return
85    status. The exception is logged to aid in debugging.
86
87    If |return_error| is set, the call will return a tuple with (default_return_value, str(error)).
88
89    Args:
90        default_return_value: What value to return in case of errors.
91        return_error: Whether to return the error string as well.
92
93    Returns:
94        Either the return value from the method call if successful or the |default_return_value| or
95        a tuple(default_return_value, str(error)).
96    """
97
98    def decorator(wrapped_function):
99        """Calls a function and catch DBus errors.
100
101        Args:
102            wrapped_function: Function to call in dbus safe context.
103
104        Returns:
105            Function return value or default_return_value on failure.
106        """
107
108        @functools.wraps(wrapped_function)
109        def wrapper(*args, **kwargs):
110            """Passes args and kwargs to a dbus safe function.
111
112            Args:
113                args: Formal python arguments.
114                kwargs: Keyword python arguments.
115
116            Returns:
117                Function return value or default_return_value on failure.
118            """
119            logging.debug('%s()', wrapped_function.__name__)
120            try:
121                return wrapped_function(*args, **kwargs)
122            except GLib.Error as e:
123                logging.debug('Exception while performing operation %s: %s', wrapped_function.__name__, e)
124
125                if return_error:
126                    return (default_return_value, str(e))
127                else:
128                    return default_return_value
129            except Exception as e:
130                logging.debug('Exception in %s: %s', wrapped_function.__name__, e)
131                logging.debug(traceback.format_exc())
132                raise
133
134        return wrapper
135
136    return decorator
137
138
139def generate_dbus_cb_objpath(name, hci=None):
140    """Generates a DBus callbacks object path with a suffix that won't conflict.
141
142    Args:
143        name:
144            The last component of the path. Note that the suffix is appended right after this.
145        hci:
146            The hci number. If specified, an additional 'hciX' component is added before @name.
147
148    Returns:
149        dbus callback object path.
150    """
151    time_ms = int(time.time() * 1000)
152    if hci is None:
153        return '/org/chromium/bluetooth/{}{}'.format(name, time_ms)
154    return '/org/chromium/bluetooth/hci{}/{}{}'.format(hci, name, time_ms)
155
156
157def dbus_optional_value(value_format, value):
158    """Makes a struct for optional value D-Bus.
159
160    Args:
161        value_format:
162            D-Bus format string (ex: a{sv}).
163        value:
164            The value to convert.
165
166    Returns:
167        An empty dictionary if value is None, otherwise dictionary
168        of optional value.
169    """
170    if not value:
171        return {}
172    return {'optional_value': GLib.Variant(value_format, value)}
173
174
175def make_kv_optional_value(value):
176    """Makes a struct for optional value D-Bus with 'a{sv}' format.
177
178    Args:
179        value:
180            The value to convert.
181
182    Returns:
183        An empty dictionary if value is None, otherwise dictionary
184        of optional value.
185    """
186    return dbus_optional_value('a{sv}', value)
187
188
189class GlibDeadlockException(Exception):
190    """Detected a situation that will cause a deadlock in GLib.
191
192    This exception should be emitted when we detect that a deadlock is likely to
193    occur. For example, a method call running in the mainloop context is making
194    a function call that is wrapped with @glib_call.
195    """
196    pass
197
198
199def glib_call(default_result=None, timeout=GLIB_METHOD_CALL_TIMEOUT, thread_name=GLIB_THREAD_NAME):
200    """Threads method call to glib thread and waits for result.
201
202    The dbus-python package does not support multi-threaded access. As a result,
203    we pipe all dbus function to the mainloop using GLib.idle_add which runs the
204    method as part of the mainloop.
205
206    Args:
207        default_result:
208            The default return value from the function call if it fails or times out.
209        timeout:
210            How long to wait for the method call to complete.
211        thread_name:
212            Name of the thread that should be running GLib.Mainloop.
213    """
214
215    def decorator(method):
216        """Internal wrapper."""
217
218        def call_and_signal(data):
219            """Calls a function and signals completion.
220
221            This method is called by GLib and added via GLib.idle_add. It will
222            be run in the same thread as the GLib mainloop.
223
224            Args:
225                data:
226                    Dict containing data to be passed. Must have keys:
227                    event, method, args, kwargs and result. The value for
228                    result should be the default value and will be set
229                    before return.
230
231            Returns:
232                False so that glib doesn't reschedule this to run again.
233            """
234            (event, method, args, kwargs) = (data['event'], data['method'], data['args'], data['kwargs'])
235            logging.info('%s: Running %s', threading.current_thread().name, str(method))
236            err = None
237            try:
238                data['result'] = method(*args, **kwargs)
239            except Exception as e:
240                logging.error('Exception during %s: %s', str(method), str(e))
241                err = e
242
243            event.set()
244
245            # If method callback is set, this will call that method with results
246            # of this method call and any error that may have resulted.
247            if 'method_callback' in data:
248                data['method_callback'](err, data['result'])
249
250            return False
251
252        @functools.wraps(method)
253        def wrapper(*args, **kwargs):
254            """Sends method call to GLib and waits for its completion.
255
256            Args:
257                *args:
258                    Positional arguments to method.
259                **kwargs:
260                    Keyword arguments to method. Some special keywords:
261                    |method_callback|: Returns result via callback without blocking.
262            """
263
264            method_callback = None
265            # If a method callback is given, we will not block on the completion
266            # of the call but expect the response in the callback instead. The
267            # callback has the signature: def callback(err, result)
268            if 'method_callback' in kwargs:
269                method_callback = kwargs['method_callback']
270                del kwargs['method_callback']
271
272            # Make sure we're not scheduling in the GLib thread since that'll
273            # cause a deadlock. An exception is if we have a method callback
274            # which is async.
275            current_thread_name = threading.current_thread().name
276            if current_thread_name is thread_name and not method_callback:
277                raise GlibDeadlockException('{} called in GLib thread'.format(method))
278
279            done_event = threading.Event()
280            data = {
281                'event': done_event,
282                'method': method,
283                'args': args,
284                'kwargs': kwargs,
285                'result': default_result,
286            }
287            if method_callback:
288                data['method_callback'] = method_callback
289
290            logging.info('%s: Adding %s to GLib.idle_add', threading.current_thread().name, str(method))
291            GLib.idle_add(call_and_signal, data)
292
293            if not method_callback:
294                # Wait for the result from the GLib call
295                if not done_event.wait(timeout=timeout):
296                    logging.warning('%s timed out after %d s', str(method), timeout)
297
298            return data['result']
299
300        return wrapper
301
302    return decorator
303
304
305def glib_callback(thread_name=GLIB_THREAD_NAME):
306    """Marks callbacks that are called by GLib and checks for errors."""
307
308    def _decorator(method):
309
310        @functools.wraps(method)
311        def _wrapper(*args, **kwargs):
312            current_thread_name = threading.current_thread().name
313            if current_thread_name is not thread_name:
314                raise GlibDeadlockException('{} should be called by GLib'.format(method))
315
316            return method(*args, **kwargs)
317
318        return _wrapper
319
320    return _decorator
321
322
323class PropertySet:
324    """Helper class with getters and setters for properties."""
325
326    class MissingProperty(Exception):
327        """Raised when property is missing in PropertySet."""
328        pass
329
330    class PropertyGetterMissing(Exception):
331        """Raised when get is called on a property that doesn't support it."""
332        pass
333
334    class PropertySetterMissing(Exception):
335        """Raised when set is called on a property that doesn't support it."""
336        pass
337
338    def __init__(self, property_set):
339        """Constructor.
340
341        Args:
342            property_set:
343                Dictionary with proxy methods for get/set of named
344                properties. These are NOT normal DBus properties
345                that are implemented via org.freedesktop.DBus.Properties.
346        """
347        self.pset = property_set
348
349    def get_property_names(self):
350        """Gets all registered properties names."""
351
352        return self.pset.keys()
353
354    def get(self, prop_name, *args):
355        """Calls the getter function for a property if it exists.
356
357        Args:
358            prop_name:
359                The property name to call the getter function on.
360            *args:
361                Any positional arguments to pass to getter function.
362
363        Raises:
364            self.MissingProperty: Raised when property is missing in PropertySet.
365            self.PropertyGetterMissing: Raised when get is called on a property that doesn't support it.
366
367        Returns:
368            Result from calling the getter function with given args.
369        """
370        if prop_name not in self.pset:
371            raise self.MissingProperty('{} is unknown.'.format(prop_name))
372
373        (getter, _) = self.pset[prop_name]
374
375        if not getter:
376            raise self.PropertyGetterMissing('{} has no getter.'.format(prop_name))
377
378        return getter(*args)
379
380    def set(self, prop_name, *args):
381        """Calls the setter function for a property if it exists.
382
383        Args:
384            prop_name:
385                The property name to call the setter function on.
386            *args:
387                Any positional arguments to pass to the setter function.
388
389        Raises:
390            self.MissingProperty: Raised when property is missing in PropertySet.
391            self.PropertySetterMissing: Raised when set is called on a property that doesn't support it.
392
393        Returns:
394            Result from calling the setter function with given args.
395        """
396        if prop_name not in self.pset:
397            raise self.MissingProperty('{} is unknown.'.format(prop_name))
398
399        (_, setter) = self.pset[prop_name]
400
401        if not setter:
402            raise self.PropertySetterMissing('{} has no getter.'.format(prop_name))
403
404        return setter(*args)
405
406
407class Connection:
408    """A Bluetooth connection."""
409
410    def __init__(self, address: str, transport: floss_enums.BtTransport):
411        self.address = address
412        self.transport = transport
413
414
415def connection_to(connection: Connection):
416    """Converts Connection from Floss format to gRPC format."""
417    internal_connection_ref = os_pb2.InternalConnectionRef(address=address_to(connection.address),
418                                                           transport=connection.transport)
419    cookie = any_pb2.Any(value=internal_connection_ref.SerializeToString())
420    return host_pb2.Connection(cookie=cookie)
421
422
423def connection_from(connection: host_pb2.Connection):
424    """Converts Connection from gRPC format to Floss format."""
425    internal_connection_ref = os_pb2.InternalConnectionRef()
426    internal_connection_ref.ParseFromString(connection.cookie.value)
427    return Connection(address=address_from(internal_connection_ref.address),
428                      transport=internal_connection_ref.transport)
429
430
431def address_from(request_address: bytes):
432    """Converts address from gRPC format to Floss format."""
433    address = request_address.hex()
434    address = f'{address[:2]}:{address[2:4]}:{address[4:6]}:{address[6:8]}:{address[8:10]}:{address[10:12]}'
435    return address.upper()
436
437
438def address_to(address: str):
439    """Converts address from Floss format to gRPC format."""
440    request_address = bytes.fromhex(address.replace(':', ''))
441    return request_address
442
443
444def uuid16_to_uuid128(uuid16: str):
445    return f'0000{uuid16}-0000-1000-8000-00805f9b34fb'
446
447
448def uuid32_to_uuid128(uuid32: str):
449    return f'{uuid32}-0000-1000-8000-00805f9b34fb'
450
451
452def get_uuid_as_list(str_uuid):
453    """Converts string uuid to a list of bytes.
454
455    Args:
456        str_uuid: String UUID.
457
458    Returns:
459        UUID string as list of bytes.
460    """
461    return list(uuid.UUID(str_uuid).bytes)
462
463
464def advertise_data_from(request_data: host_pb2.DataTypes):
465    """Mapping DataTypes to a dict.
466
467    The dict content follows the format of Floss AdvertiseData.
468
469    Args:
470        request_data : advertising data.
471
472    Raises:
473        NotImplementedError: if request data is not implemented.
474
475    Returns:
476        dict: advertising data.
477    """
478    advertise_data = {
479        'service_uuids': [],
480        'solicit_uuids': [],
481        'transport_discovery_data': [],
482        'manufacturer_data': {},
483        'service_data': {},
484        'include_tx_power_level': False,
485        'include_device_name': False,
486    }
487
488    # incomplete_service_class_uuids
489    if (request_data.incomplete_service_class_uuids16 or request_data.incomplete_service_class_uuids32 or
490            request_data.incomplete_service_class_uuids128):
491        raise NotImplementedError('Incomplete service class uuid not supported')
492
493    # service_uuids
494    for uuid16 in request_data.complete_service_class_uuids16:
495        advertise_data['service_uuids'].append(uuid16_to_uuid128(uuid16))
496
497    for uuid32 in request_data.complete_service_class_uuids32:
498        advertise_data['service_uuids'].append(uuid32_to_uuid128(uuid32))
499
500    for uuid128 in request_data.complete_service_class_uuids128:
501        advertise_data['service_uuids'].append(uuid128)
502
503    # solicit_uuids
504    for uuid16 in request_data.service_solicitation_uuids16:
505        advertise_data['solicit_uuids'].append(uuid16_to_uuid128(uuid16))
506
507    for uuid32 in request_data.service_solicitation_uuids32:
508        advertise_data['solicit_uuids'].append(uuid32_to_uuid128(uuid32))
509
510    for uuid128 in request_data.service_solicitation_uuids128:
511        advertise_data['solicit_uuids'].append(uuid128)
512
513    # service_data
514    for (uuid16, data) in request_data.service_data_uuid16:
515        advertise_data['service_data'][uuid16_to_uuid128(uuid16)] = data
516
517    for (uuid32, data) in request_data.service_data_uuid32:
518        advertise_data['service_data'][uuid32_to_uuid128(uuid32)] = data
519
520    for (uuid128, data) in request_data.service_data_uuid128:
521        advertise_data['service_data'][uuid128] = data
522
523    advertise_data['manufacturer_data'][hex(
524        floss_enums.CompanyIdentifiers.GOOGLE)] = request_data.manufacturer_specific_data
525
526    # The name is derived from adapter directly in floss.
527    if request_data.WhichOneof('shortened_local_name_oneof') in ('include_shortened_local_name',
528                                                                 'include_complete_local_name'):
529        advertise_data['include_device_name'] = getattr(request_data,
530                                                        request_data.WhichOneof('shortened_local_name_oneof')).value
531
532    # The tx power level is decided by the lower layers.
533    if request_data.WhichOneof('tx_power_level_oneof') == 'include_tx_power_level':
534        advertise_data['include_tx_power_level'] = request_data.include_tx_power_level
535    return advertise_data
536
537
538def create_observer_name(observer):
539    """Generates an unique name for an observer.
540
541    Args:
542        observer: an observer class to observer the bluetooth callbacks.
543
544    Returns:
545        str: an unique name.
546    """
547    return observer.__class__.__name__ + str(id(observer))
548
549
550# anext build-in is new in python3.10. Deprecate this function
551# when we are able to use it.
552async def anext(ait):
553    return await ait.__anext__()
554
555
556def parse_advertiging_data(adv_data: List[int]) -> host_pb2.DataTypes:
557    index = 0
558    data = host_pb2.DataTypes()
559
560    # advertising data packet is repeated by many advertising data and each one with the following format:
561    # | Length (0) | Type (1) | Data Payload (2~N-1)
562    # Take an advertising data packet [2, 1, 6, 5, 3, 0, 24, 1, 24] as an example,
563    # The first 3 numbers 2, 1, 6:
564    # 2 is the data length is 2 including the data type,
565    # 1 is the data type is Flags (0x01),
566    # 6 is the data payload.
567    while index < len(adv_data):
568        # Extract data length.
569        data_length = adv_data[index]
570        index = index + 1
571
572        if data_length <= 0:
573            break
574
575        # Extract data type.
576        data_type = adv_data[index]
577        index = index + 1
578
579        # Extract data payload.
580        if data_type == floss_enums.AdvertisingDataType.COMPLETE_LOCAL_NAME:
581            data.complete_local_name = parse_complete_local_name(adv_data[index:index + data_length - 1])
582            logging.info('complete_local_name: %s', data.complete_local_name)
583        else:
584            logging.debug('Unsupported advertising data type to parse: %s', data_type)
585
586        index = index + data_length - 1
587
588    logging.info('Parsed data: %s', data)
589    return data
590
591
592def parse_complete_local_name(data: List[int]) -> Optional[str]:
593    return ''.join(chr(char) for char in data) if data else None
594