1#!/usr/bin/env python
2
3# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7import dbus
8import dbus.mainloop.glib
9import dbus.service
10import gobject
11import json
12import logging
13import logging.handlers
14import os
15import shutil
16import time
17
18import common
19from autotest_lib.client.bin import utils
20from autotest_lib.client.common_lib.cros.bluetooth import bluetooth_socket
21from autotest_lib.client.cros import constants
22from autotest_lib.client.cros import xmlrpc_server
23from autotest_lib.client.cros.bluetooth import advertisement
24from autotest_lib.client.cros.bluetooth import output_recorder
25
26
27class PairingAgent(dbus.service.Object):
28    """The agent handling the authentication process of bluetooth pairing.
29
30    PairingAgent overrides RequestPinCode method to return a given pin code.
31    User can use this agent to pair bluetooth device which has a known
32    pin code.
33
34    TODO (josephsih): more pairing modes other than pin code would be
35    supported later.
36
37    """
38
39    def __init__(self, pin, *args, **kwargs):
40        super(PairingAgent, self).__init__(*args, **kwargs)
41        self._pin = pin
42
43
44    @dbus.service.method('org.bluez.Agent1',
45                         in_signature='o', out_signature='s')
46    def RequestPinCode(self, device_path):
47        """Requests pin code for a device.
48
49        Returns the known pin code for the request.
50
51        @param device_path: The object path of the device.
52
53        @returns: The known pin code.
54
55        """
56        logging.info('RequestPinCode for %s; return %s', device_path, self._pin)
57        return self._pin
58
59
60class BluetoothDeviceXmlRpcDelegate(xmlrpc_server.XmlRpcDelegate):
61    """Exposes DUT methods called remotely during Bluetooth autotests.
62
63    All instance methods of this object without a preceding '_' are exposed via
64    an XML-RPC server. This is not a stateless handler object, which means that
65    if you store state inside the delegate, that state will remain around for
66    future calls.
67    """
68
69    UPSTART_PATH = 'unix:abstract=/com/ubuntu/upstart'
70    UPSTART_MANAGER_PATH = '/com/ubuntu/Upstart'
71    UPSTART_MANAGER_IFACE = 'com.ubuntu.Upstart0_6'
72    UPSTART_JOB_IFACE = 'com.ubuntu.Upstart0_6.Job'
73
74    UPSTART_ERROR_UNKNOWNINSTANCE = \
75            'com.ubuntu.Upstart0_6.Error.UnknownInstance'
76    UPSTART_ERROR_ALREADYSTARTED = \
77            'com.ubuntu.Upstart0_6.Error.AlreadyStarted'
78
79    BLUETOOTHD_JOB = 'bluetoothd'
80
81    DBUS_ERROR_SERVICEUNKNOWN = 'org.freedesktop.DBus.Error.ServiceUnknown'
82
83    BLUEZ_SERVICE_NAME = 'org.bluez'
84    BLUEZ_MANAGER_PATH = '/'
85    BLUEZ_MANAGER_IFACE = 'org.freedesktop.DBus.ObjectManager'
86    BLUEZ_ADAPTER_IFACE = 'org.bluez.Adapter1'
87    BLUEZ_DEVICE_IFACE = 'org.bluez.Device1'
88    BLUEZ_LE_ADVERTISING_MANAGER_IFACE = 'org.bluez.LEAdvertisingManager1'
89    BLUEZ_AGENT_MANAGER_PATH = '/org/bluez'
90    BLUEZ_AGENT_MANAGER_IFACE = 'org.bluez.AgentManager1'
91    BLUEZ_PROFILE_MANAGER_PATH = '/org/bluez'
92    BLUEZ_PROFILE_MANAGER_IFACE = 'org.bluez.ProfileManager1'
93    BLUEZ_ERROR_ALREADY_EXISTS = 'org.bluez.Error.AlreadyExists'
94    DBUS_PROP_IFACE = 'org.freedesktop.DBus.Properties'
95    AGENT_PATH = '/test/agent'
96
97    BLUETOOTH_LIBDIR = '/var/lib/bluetooth'
98    BTMON_STOP_DELAY_SECS = 3
99
100    # Timeout for how long we'll wait for BlueZ and the Adapter to show up
101    # after reset.
102    ADAPTER_TIMEOUT = 30
103
104    def __init__(self):
105        super(BluetoothDeviceXmlRpcDelegate, self).__init__()
106
107        # Open the Bluetooth Raw socket to the kernel which provides us direct,
108        # raw, access to the HCI controller.
109        self._raw = bluetooth_socket.BluetoothRawSocket()
110
111        # Open the Bluetooth Control socket to the kernel which provides us
112        # raw management access to the Bluetooth Host Subsystem. Read the list
113        # of adapter indexes to determine whether or not this device has a
114        # Bluetooth Adapter or not.
115        self._control = bluetooth_socket.BluetoothControlSocket()
116        self._has_adapter = len(self._control.read_index_list()) > 0
117
118        # Set up the connection to Upstart so we can start and stop services
119        # and fetch the bluetoothd job.
120        self._upstart_conn = dbus.connection.Connection(self.UPSTART_PATH)
121        self._upstart = self._upstart_conn.get_object(
122                None,
123                self.UPSTART_MANAGER_PATH)
124
125        bluetoothd_path = self._upstart.GetJobByName(
126                self.BLUETOOTHD_JOB,
127                dbus_interface=self.UPSTART_MANAGER_IFACE)
128        self._bluetoothd = self._upstart_conn.get_object(
129                None,
130                bluetoothd_path)
131
132        # Arrange for the GLib main loop to be the default.
133        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
134
135        # Set up the connection to the D-Bus System Bus, get the object for
136        # the Bluetooth Userspace Daemon (BlueZ) and that daemon's object for
137        # the Bluetooth Adapter, and the advertising manager.
138        self._system_bus = dbus.SystemBus()
139        self._update_bluez()
140        self._update_adapter()
141        self._update_advertising()
142
143        # The agent to handle pin code request, which will be
144        # created when user calls pair_legacy_device method.
145        self._pairing_agent = None
146        # The default capability of the agent.
147        self._capability = 'KeyboardDisplay'
148
149        # Initailize a btmon object to record bluetoothd's activity.
150        self.btmon = output_recorder.OutputRecorder(
151                'btmon', stop_delay_secs=self.BTMON_STOP_DELAY_SECS)
152
153        self.advertisements = []
154        self._adv_mainloop = gobject.MainLoop()
155
156
157    @xmlrpc_server.dbus_safe(False)
158    def start_bluetoothd(self):
159        """start bluetoothd.
160
161        This includes powering up the adapter.
162
163        @returns: True if bluetoothd is started correctly.
164                  False otherwise.
165
166        """
167        try:
168            self._bluetoothd.Start(dbus.Array(signature='s'), True,
169                                   dbus_interface=self.UPSTART_JOB_IFACE)
170        except dbus.exceptions.DBusException as e:
171            # if bluetoothd was already started, the exception looks like
172            #     dbus.exceptions.DBusException:
173            #     com.ubuntu.Upstart0_6.Error.AlreadyStarted: Job is already
174            #     running: bluetoothd
175            if e.get_dbus_name() != self.UPSTART_ERROR_ALREADYSTARTED:
176                logging.error('Error starting bluetoothd: %s', e)
177                return False
178
179        logging.debug('waiting for bluez start')
180        try:
181            utils.poll_for_condition(
182                    condition=self._update_bluez,
183                    desc='Bluetooth Daemon has started.',
184                    timeout=self.ADAPTER_TIMEOUT)
185        except Exception as e:
186            logging.error('timeout: error starting bluetoothd: %s', e)
187            return False
188
189        # Waiting for the self._adapter object.
190        # This does not mean that the adapter is powered on.
191        logging.debug('waiting for bluez to obtain adapter information')
192        try:
193            utils.poll_for_condition(
194                    condition=self._update_adapter,
195                    desc='Bluetooth Daemon has adapter information.',
196                    timeout=self.ADAPTER_TIMEOUT)
197        except Exception as e:
198            logging.error('timeout: error starting adapter: %s', e)
199            return False
200
201        # Waiting for the self._advertising interface object.
202        logging.debug('waiting for bluez to obtain interface manager.')
203        try:
204            utils.poll_for_condition(
205                    condition=self._update_advertising,
206                    desc='Bluetooth Daemon has advertising interface.',
207                    timeout=self.ADAPTER_TIMEOUT)
208        except utils.TimeoutError:
209            logging.error('timeout: error getting advertising interface')
210            return False
211
212        return True
213
214
215    @xmlrpc_server.dbus_safe(False)
216    def stop_bluetoothd(self):
217        """stop bluetoothd.
218
219        @returns: True if bluetoothd is stopped correctly.
220                  False otherwise.
221
222        """
223        def bluez_stopped():
224            """Checks the bluetooth daemon status.
225
226            @returns: True if bluez is stopped. False otherwise.
227
228            """
229            return not self._update_bluez()
230
231        try:
232            self._bluetoothd.Stop(dbus.Array(signature='s'), True,
233                                  dbus_interface=self.UPSTART_JOB_IFACE)
234        except dbus.exceptions.DBusException as e:
235            # If bluetoothd was stopped already, the exception looks like
236            #    dbus.exceptions.DBusException:
237            #    com.ubuntu.Upstart0_6.Error.UnknownInstance: Unknown instance:
238            if e.get_dbus_name() != self.UPSTART_ERROR_UNKNOWNINSTANCE:
239                logging.error('Error stopping bluetoothd!')
240                return False
241
242        logging.debug('waiting for bluez stop')
243        try:
244            utils.poll_for_condition(
245                    condition=bluez_stopped,
246                    desc='Bluetooth Daemon has stopped.',
247                    timeout=self.ADAPTER_TIMEOUT)
248            bluetoothd_stopped = True
249        except Exception as e:
250            logging.error('timeout: error stopping bluetoothd: %s', e)
251            bluetoothd_stopped = False
252
253        return bluetoothd_stopped
254
255
256    def is_bluetoothd_running(self):
257        """Is bluetoothd running?
258
259        @returns: True if bluetoothd is running
260
261        """
262        return bool(self._get_dbus_proxy_for_bluetoothd())
263
264
265    def _update_bluez(self):
266        """Store a D-Bus proxy for the Bluetooth daemon in self._bluez.
267
268        This may be called in a loop until it returns True to wait for the
269        daemon to be ready after it has been started.
270
271        @return True on success, False otherwise.
272
273        """
274        self._bluez = self._get_dbus_proxy_for_bluetoothd()
275        return bool(self._bluez)
276
277
278    @xmlrpc_server.dbus_safe(False)
279    def _get_dbus_proxy_for_bluetoothd(self):
280        """Get the D-Bus proxy for the Bluetooth daemon.
281
282        @return True on success, False otherwise.
283
284        """
285        bluez = None
286        try:
287            bluez = self._system_bus.get_object(self.BLUEZ_SERVICE_NAME,
288                                                self.BLUEZ_MANAGER_PATH)
289            logging.debug('bluetoothd is running')
290        except dbus.exceptions.DBusException as e:
291            # When bluetoothd is not running, the exception looks like
292            #     dbus.exceptions.DBusException:
293            #     org.freedesktop.DBus.Error.ServiceUnknown: The name org.bluez
294            #     was not provided by any .service files
295            if e.get_dbus_name() == self.DBUS_ERROR_SERVICEUNKNOWN:
296                logging.debug('bluetoothd is not running')
297            else:
298                logging.error('Error getting dbus proxy for Bluez: %s', e)
299        return bluez
300
301
302    def _update_adapter(self):
303        """Store a D-Bus proxy for the local adapter in self._adapter.
304
305        This may be called in a loop until it returns True to wait for the
306        daemon to be ready, and have obtained the adapter information itself,
307        after it has been started.
308
309        Since not all devices will have adapters, this will also return True
310        in the case where we have obtained an empty adapter index list from the
311        kernel.
312
313        Note that this method does not power on the adapter.
314
315        @return True on success, including if there is no local adapter,
316            False otherwise.
317
318        """
319        self._adapter = None
320        if self._bluez is None:
321            logging.warning('Bluez not found!')
322            return False
323        if not self._has_adapter:
324            logging.debug('Device has no adapter; returning')
325            return True
326        self._adapter = self._get_adapter()
327        return bool(self._adapter)
328
329    def _update_advertising(self):
330        """Store a D-Bus proxy for the local advertising interface manager.
331
332        This may be called repeatedly in a loop until True is returned;
333        otherwise we wait for bluetoothd to start. After bluetoothd starts, we
334        check the existence of a local adapter and proceed to get the
335        advertisement interface manager.
336
337        Since not all devices will have adapters, this will also return True
338        in the case where there is no adapter.
339
340        @return True on success, including if there is no local adapter,
341                False otherwise.
342
343        """
344        self._advertising = None
345        if self._bluez is None:
346            logging.warning('Bluez not found!')
347            return False
348        if not self._has_adapter:
349            logging.debug('Device has no adapter; returning')
350            return True
351        self._advertising = self._get_advertising()
352        return bool(self._advertising)
353
354
355    @xmlrpc_server.dbus_safe(False)
356    def _get_adapter(self):
357        """Get the D-Bus proxy for the local adapter.
358
359        @return the adapter on success. None otherwise.
360
361        """
362        objects = self._bluez.GetManagedObjects(
363                dbus_interface=self.BLUEZ_MANAGER_IFACE)
364        for path, ifaces in objects.iteritems():
365            logging.debug('%s -> %r', path, ifaces.keys())
366            if self.BLUEZ_ADAPTER_IFACE in ifaces:
367                logging.debug('using adapter %s', path)
368                adapter = self._system_bus.get_object(
369                        self.BLUEZ_SERVICE_NAME,
370                        path)
371                return adapter
372        else:
373            logging.warning('No adapter found in interface!')
374            return None
375
376
377    @xmlrpc_server.dbus_safe(False)
378    def _get_advertising(self):
379        """Get the D-Bus proxy for the local advertising interface.
380
381        @return the advertising interface object.
382
383        """
384        return dbus.Interface(self._adapter,
385                              self.BLUEZ_LE_ADVERTISING_MANAGER_IFACE)
386
387
388    @xmlrpc_server.dbus_safe(False)
389    def reset_on(self):
390        """Reset the adapter and settings and power up the adapter.
391
392        @return True on success, False otherwise.
393
394        """
395        return self._reset(set_power=True)
396
397
398    @xmlrpc_server.dbus_safe(False)
399    def reset_off(self):
400        """Reset the adapter and settings, leave the adapter powered off.
401
402        @return True on success, False otherwise.
403
404        """
405        return self._reset(set_power=False)
406
407
408    def has_adapter(self):
409        """Return if an adapter is present.
410
411        This will only return True if we have determined both that there is
412        a Bluetooth adapter on this device (kernel adapter index list is not
413        empty) and that the Bluetooth daemon has exported an object for it.
414
415        @return True if an adapter is present, False if not.
416
417        """
418        return self._has_adapter and self._adapter is not None
419
420
421    def _reset(self, set_power=False):
422        """Reset the Bluetooth adapter and settings.
423
424        @param set_power: adapter power state to set (True or False).
425
426        @return True on success, False otherwise.
427
428        """
429        logging.debug('_reset')
430
431        # Power off the adapter before stopping the bluetoothd.
432        if self._adapter and not set_power:
433            self._set_powered(False)
434
435        # Stop bluetoothd.
436        if not self.stop_bluetoothd():
437            return False
438
439        # Remove the settings and cached devices.
440        try:
441            for subdir in os.listdir(self.BLUETOOTH_LIBDIR):
442                shutil.rmtree(os.path.join(self.BLUETOOTH_LIBDIR, subdir))
443            remove_settings = True
444        except Exception as e:
445            logging.error('Error in removing subdirs in %s: %s.',
446                          self.BLUETOOTH_LIBDIR, e)
447            remove_settings = False
448
449        # Start bluetoothd.
450        if not self.start_bluetoothd():
451            return False
452
453        # Power on the adapter after restarting the bluetoothd.
454        if self._adapter and set_power:
455            self._set_powered(True)
456
457        return remove_settings
458
459    @xmlrpc_server.dbus_safe(False)
460    def set_powered(self, powered):
461        """Set the adapter power state.
462
463        @param powered: adapter power state to set (True or False).
464
465        @return True on success, False otherwise.
466
467        """
468        if not self._adapter:
469            if not powered:
470                # Return success if we are trying to power off an adapter that's
471                # missing or gone away, since the expected result has happened.
472                return True
473            else:
474                logging.warning('Adapter not found!')
475                return False
476        self._set_powered(powered)
477        return True
478
479
480    @xmlrpc_server.dbus_safe(False)
481    def _set_powered(self, powered):
482        """Set the adapter power state.
483
484        @param powered: adapter power state to set (True or False).
485
486        """
487        logging.debug('_set_powered %r', powered)
488        self._adapter.Set(self.BLUEZ_ADAPTER_IFACE, 'Powered', powered,
489                          dbus_interface=dbus.PROPERTIES_IFACE)
490
491
492    @xmlrpc_server.dbus_safe(False)
493    def set_discoverable(self, discoverable):
494        """Set the adapter discoverable state.
495
496        @param discoverable: adapter discoverable state to set (True or False).
497
498        @return True on success, False otherwise.
499
500        """
501        if not discoverable and not self._adapter:
502            # Return success if we are trying to make an adapter that's
503            # missing or gone away, undiscoverable, since the expected result
504            # has happened.
505            return True
506        self._adapter.Set(self.BLUEZ_ADAPTER_IFACE,
507                          'Discoverable', discoverable,
508                          dbus_interface=dbus.PROPERTIES_IFACE)
509        return True
510
511
512    @xmlrpc_server.dbus_safe(False)
513    def set_pairable(self, pairable):
514        """Set the adapter pairable state.
515
516        @param pairable: adapter pairable state to set (True or False).
517
518        @return True on success, False otherwise.
519
520        """
521        self._adapter.Set(self.BLUEZ_ADAPTER_IFACE, 'Pairable', pairable,
522                          dbus_interface=dbus.PROPERTIES_IFACE)
523        return True
524
525
526    @xmlrpc_server.dbus_safe(False)
527    def get_adapter_properties(self):
528        """Read the adapter properties from the Bluetooth Daemon.
529
530        @return the properties as a JSON-encoded dictionary on success,
531            the value False otherwise.
532
533        """
534        if self._bluez:
535            objects = self._bluez.GetManagedObjects(
536                    dbus_interface=self.BLUEZ_MANAGER_IFACE)
537            props = objects[self._adapter.object_path][self.BLUEZ_ADAPTER_IFACE]
538        else:
539            props = {}
540        logging.debug('get_adapter_properties: %s', props)
541        return json.dumps(props)
542
543
544    def read_version(self):
545        """Read the version of the management interface from the Kernel.
546
547        @return the information as a JSON-encoded tuple of:
548          ( version, revision )
549
550        """
551        return json.dumps(self._control.read_version())
552
553
554    def read_supported_commands(self):
555        """Read the set of supported commands from the Kernel.
556
557        @return the information as a JSON-encoded tuple of:
558          ( commands, events )
559
560        """
561        return json.dumps(self._control.read_supported_commands())
562
563
564    def read_index_list(self):
565        """Read the list of currently known controllers from the Kernel.
566
567        @return the information as a JSON-encoded array of controller indexes.
568
569        """
570        return json.dumps(self._control.read_index_list())
571
572
573    def read_info(self):
574        """Read the adapter information from the Kernel.
575
576        @return the information as a JSON-encoded tuple of:
577          ( address, bluetooth_version, manufacturer_id,
578            supported_settings, current_settings, class_of_device,
579            name, short_name )
580
581        """
582        return json.dumps(self._control.read_info(0))
583
584
585    def add_device(self, address, address_type, action):
586        """Add a device to the Kernel action list.
587
588        @param address: Address of the device to add.
589        @param address_type: Type of device in @address.
590        @param action: Action to take.
591
592        @return on success, a JSON-encoded typle of:
593          ( address, address_type ), None on failure.
594
595        """
596        return json.dumps(self._control.add_device(
597                0, address, address_type, action))
598
599
600    def remove_device(self, address, address_type):
601        """Remove a device from the Kernel action list.
602
603        @param address: Address of the device to remove.
604        @param address_type: Type of device in @address.
605
606        @return on success, a JSON-encoded typle of:
607          ( address, address_type ), None on failure.
608
609        """
610        return json.dumps(self._control.remove_device(
611                0, address, address_type))
612
613
614    @xmlrpc_server.dbus_safe(False)
615    def get_devices(self):
616        """Read information about remote devices known to the adapter.
617
618        @return the properties of each device as a JSON-encoded array of
619            dictionaries on success, the value False otherwise.
620
621        """
622        objects = self._bluez.GetManagedObjects(
623                dbus_interface=self.BLUEZ_MANAGER_IFACE, byte_arrays=True)
624        devices = []
625        for path, ifaces in objects.iteritems():
626            if self.BLUEZ_DEVICE_IFACE in ifaces:
627                devices.append(objects[path][self.BLUEZ_DEVICE_IFACE])
628        return json.dumps(devices)
629
630
631    @xmlrpc_server.dbus_safe(False)
632    def get_device_by_address(self, address):
633        """Read information about the remote device with the specified address.
634
635        @param address: Address of the device to get.
636
637        @return the properties of the device as a JSON-encoded dictionary
638            on success, the value False otherwise.
639
640        """
641        objects = self._bluez.GetManagedObjects(
642                dbus_interface=self.BLUEZ_MANAGER_IFACE, byte_arrays=True)
643        devices = []
644        for path, ifaces in objects.iteritems():
645            if self.BLUEZ_DEVICE_IFACE in ifaces:
646                device = objects[path][self.BLUEZ_DEVICE_IFACE]
647                if device.get('Address') == address:
648                    return json.dumps(device)
649
650        devices = json.loads(self.get_devices())
651        for device in devices:
652            if device.get['Address'] == address:
653                return json.dumps(device)
654        return json.dumps(dict())
655
656
657    @xmlrpc_server.dbus_safe(False)
658    def start_discovery(self):
659        """Start discovery of remote devices.
660
661        Obtain the discovered device information using get_devices(), called
662        stop_discovery() when done.
663
664        @return True on success, False otherwise.
665
666        """
667        if not self._adapter:
668            return False
669        self._adapter.StartDiscovery(dbus_interface=self.BLUEZ_ADAPTER_IFACE)
670        return True
671
672
673    @xmlrpc_server.dbus_safe(False)
674    def stop_discovery(self):
675        """Stop discovery of remote devices.
676
677        @return True on success, False otherwise.
678
679        """
680        if not self._adapter:
681            return False
682        self._adapter.StopDiscovery(dbus_interface=self.BLUEZ_ADAPTER_IFACE)
683        return True
684
685
686    def get_dev_info(self):
687        """Read raw HCI device information.
688
689        @return JSON-encoded tuple of:
690                (index, name, address, flags, device_type, bus_type,
691                       features, pkt_type, link_policy, link_mode,
692                       acl_mtu, acl_pkts, sco_mtu, sco_pkts,
693                       err_rx, err_tx, cmd_tx, evt_rx, acl_tx, acl_rx,
694                       sco_tx, sco_rx, byte_rx, byte_tx) on success,
695                None on failure.
696
697        """
698        return json.dumps(self._raw.get_dev_info(0))
699
700
701    @xmlrpc_server.dbus_safe(False)
702    def register_profile(self, path, uuid, options):
703        """Register new profile (service).
704
705        @param path: Path to the profile object.
706        @param uuid: Service Class ID of the service as string.
707        @param options: Dictionary of options for the new service, compliant
708                        with BlueZ D-Bus Profile API standard.
709
710        @return True on success, False otherwise.
711
712        """
713        profile_manager = dbus.Interface(
714                              self._system_bus.get_object(
715                                  self.BLUEZ_SERVICE_NAME,
716                                  self.BLUEZ_PROFILE_MANAGER_PATH),
717                              self.BLUEZ_PROFILE_MANAGER_IFACE)
718        profile_manager.RegisterProfile(path, uuid, options)
719        return True
720
721
722    def has_device(self, address):
723        """Checks if the device with a given address exists.
724
725        @param address: Address of the device.
726
727        @returns: True if there is an interface object with that address.
728                  False if the device is not found.
729
730        @raises: Exception if a D-Bus error is encountered.
731
732        """
733        result = self._find_device(address)
734        logging.debug('has_device result: %s', str(result))
735
736        # The result being False indicates that there is a D-Bus error.
737        if result is False:
738            raise Exception('dbus.Interface error')
739
740        # Return True if the result is not None, e.g. a D-Bus interface object;
741        # False otherwise.
742        return bool(result)
743
744
745    @xmlrpc_server.dbus_safe(False)
746    def _find_device(self, address):
747        """Finds the device with a given address.
748
749        Find the device with a given address and returns the
750        device interface.
751
752        @param address: Address of the device.
753
754        @returns: An 'org.bluez.Device1' interface to the device.
755                  None if device can not be found.
756
757        """
758        objects = self._bluez.GetManagedObjects(
759                dbus_interface=self.BLUEZ_MANAGER_IFACE)
760        for path, ifaces in objects.iteritems():
761            device = ifaces.get(self.BLUEZ_DEVICE_IFACE)
762            if device is None:
763                continue
764            if (device['Address'] == address and
765                path.startswith(self._adapter.object_path)):
766                obj = self._system_bus.get_object(
767                        self.BLUEZ_SERVICE_NAME, path)
768                return dbus.Interface(obj, self.BLUEZ_DEVICE_IFACE)
769        logging.info('Device not found')
770        return None
771
772
773    @xmlrpc_server.dbus_safe(False)
774    def _setup_pairing_agent(self, pin):
775        """Initializes and resiters a PairingAgent to handle authenticaiton.
776
777        @param pin: The pin code this agent will answer.
778
779        """
780        if self._pairing_agent:
781            logging.info('Removing the old agent before initializing a new one')
782            self._pairing_agent.remove_from_connection()
783            self._pairing_agent = None
784        self._pairing_agent= PairingAgent(pin, self._system_bus,
785                                          self.AGENT_PATH)
786        agent_manager = dbus.Interface(
787                self._system_bus.get_object(self.BLUEZ_SERVICE_NAME,
788                                            self.BLUEZ_AGENT_MANAGER_PATH),
789                self.BLUEZ_AGENT_MANAGER_IFACE)
790        try:
791            agent_manager.RegisterAgent(self.AGENT_PATH, self._capability)
792        except dbus.exceptions.DBusException, e:
793            if e.get_dbus_name() == self.BLUEZ_ERROR_ALREADY_EXISTS:
794                logging.info('Unregistering old agent and registering the new')
795                agent_manager.UnregisterAgent(self.AGENT_PATH)
796                agent_manager.RegisterAgent(self.AGENT_PATH, self._capability)
797            else:
798                logging.error('Error setting up pin agent: %s', e)
799                raise
800        logging.info('Agent registered: %s', self.AGENT_PATH)
801
802
803    @xmlrpc_server.dbus_safe(False)
804    def _is_paired(self,  device):
805        """Checks if a device is paired.
806
807        @param device: An 'org.bluez.Device1' interface to the device.
808
809        @returns: True if device is paired. False otherwise.
810
811        """
812        props = dbus.Interface(device, dbus.PROPERTIES_IFACE)
813        paired = props.Get(self.BLUEZ_DEVICE_IFACE, 'Paired')
814        return bool(paired)
815
816
817    @xmlrpc_server.dbus_safe(False)
818    def device_is_paired(self, address):
819        """Checks if a device is paired.
820
821        @param address: address of the device.
822
823        @returns: True if device is paired. False otherwise.
824
825        """
826        device = self._find_device(address)
827        if not device:
828            logging.error('Device not found')
829            return False
830        return self._is_paired(device)
831
832
833    @xmlrpc_server.dbus_safe(False)
834    def _is_connected(self,  device):
835        """Checks if a device is connected.
836
837        @param device: An 'org.bluez.Device1' interface to the device.
838
839        @returns: True if device is connected. False otherwise.
840
841        """
842        props = dbus.Interface(device, dbus.PROPERTIES_IFACE)
843        connected = props.Get(self.BLUEZ_DEVICE_IFACE, 'Connected')
844        logging.info('Got connected = %r', connected)
845        return bool(connected)
846
847
848    @xmlrpc_server.dbus_safe(False)
849    def _set_trusted_by_device(self, device, trusted=True):
850        """Set the device trusted by device object.
851
852        @param device: the device object to set trusted.
853        @param trusted: True or False indicating whether to set trusted or not.
854
855        @returns: True if successful. False otherwise.
856
857        """
858        try:
859            properties = dbus.Interface(device, self.DBUS_PROP_IFACE)
860            properties.Set(self.BLUEZ_DEVICE_IFACE, 'Trusted', trusted)
861            return True
862        except Exception as e:
863            logging.error('_set_trusted_by_device: %s', e)
864        except:
865            logging.error('_set_trusted_by_device: unexpected error')
866        return False
867
868
869    @xmlrpc_server.dbus_safe(False)
870    def _set_trusted_by_path(self, device_path, trusted=True):
871        """Set the device trusted by the device path.
872
873        @param device_path: the object path of the device.
874        @param trusted: True or False indicating whether to set trusted or not.
875
876        @returns: True if successful. False otherwise.
877
878        """
879        try:
880            device = self._system_bus.get_object(self.BLUEZ_SERVICE_NAME,
881                                                 device_path)
882            return self._set_trusted_by_device(device, trusted)
883        except Exception as e:
884            logging.error('_set_trusted_by_path: %s', e)
885        except:
886            logging.error('_set_trusted_by_path: unexpected error')
887        return False
888
889
890    @xmlrpc_server.dbus_safe(False)
891    def set_trusted(self, address, trusted=True):
892        """Set the device trusted by address.
893
894        @param address: The bluetooth address of the device.
895        @param trusted: True or False indicating whether to set trusted or not.
896
897        @returns: True if successful. False otherwise.
898
899        """
900        try:
901            device = self._find_device(address)
902            return self._set_trusted_by_device(device, trusted)
903        except Exception as e:
904            logging.error('set_trusted: %s', e)
905        except:
906            logging.error('set_trusted: unexpected error')
907        return False
908
909
910    @xmlrpc_server.dbus_safe(False)
911    def pair_legacy_device(self, address, pin, trusted, timeout=60):
912        """Pairs a device with a given pin code.
913
914        Registers a agent who handles pin code request and
915        pairs a device with known pin code.
916
917        Note that the adapter does not automatically connnect to the device
918        when pairing is done. The connect_device() method has to be invoked
919        explicitly to connect to the device. This provides finer control
920        for testing purpose.
921
922        @param address: Address of the device to pair.
923        @param pin: The pin code of the device to pair.
924        @param trusted: indicating whether to set the device trusted.
925        @param timeout: The timeout in seconds for pairing.
926
927        @returns: True on success. False otherwise.
928
929        """
930        device = self._find_device(address)
931        if not device:
932            logging.error('Device not found')
933            return False
934        if self._is_paired(device):
935            logging.info('Device is already paired')
936            return True
937
938        device_path = device.object_path
939        logging.info('Device %s is found.' % device.object_path)
940
941        self._setup_pairing_agent(pin)
942        mainloop = gobject.MainLoop()
943
944
945        def pair_reply():
946            """Handler when pairing succeeded."""
947            logging.info('Device paired: %s', device_path)
948            if trusted:
949                self._set_trusted_by_path(device_path, trusted=True)
950                logging.info('Device trusted: %s', device_path)
951            mainloop.quit()
952
953
954        def pair_error(error):
955            """Handler when pairing failed.
956
957            @param error: one of errors defined in org.bluez.Error representing
958                          the error in pairing.
959
960            """
961            try:
962                error_name = error.get_dbus_name()
963                if error_name == 'org.freedesktop.DBus.Error.NoReply':
964                    logging.error('Timed out after %d ms. Cancelling pairing.',
965                                  timeout)
966                    device.CancelPairing()
967                else:
968                    logging.error('Pairing device failed: %s', error)
969            finally:
970                mainloop.quit()
971
972
973        device.Pair(reply_handler=pair_reply, error_handler=pair_error,
974                    timeout=timeout * 1000)
975        mainloop.run()
976        return self._is_paired(device)
977
978
979    @xmlrpc_server.dbus_safe(False)
980    def remove_device_object(self, address):
981        """Removes a device object and the pairing information.
982
983        Calls RemoveDevice method to remove remote device
984        object and the pairing information.
985
986        @param address: Address of the device to unpair.
987
988        @returns: True on success. False otherwise.
989
990        """
991        device = self._find_device(address)
992        if not device:
993            logging.error('Device not found')
994            return False
995        self._adapter.RemoveDevice(
996                device.object_path, dbus_interface=self.BLUEZ_ADAPTER_IFACE)
997        return True
998
999
1000    @xmlrpc_server.dbus_safe(False)
1001    def connect_device(self, address):
1002        """Connects a device.
1003
1004        Connects a device if it is not connected.
1005
1006        @param address: Address of the device to connect.
1007
1008        @returns: True on success. False otherwise.
1009
1010        """
1011        device = self._find_device(address)
1012        if not device:
1013            logging.error('Device not found')
1014            return False
1015        if self._is_connected(device):
1016          logging.info('Device is already connected')
1017          return True
1018        device.Connect()
1019        return self._is_connected(device)
1020
1021
1022    @xmlrpc_server.dbus_safe(False)
1023    def device_is_connected(self, address):
1024        """Checks if a device is connected.
1025
1026        @param address: Address of the device to connect.
1027
1028        @returns: True if device is connected. False otherwise.
1029
1030        """
1031        device = self._find_device(address)
1032        if not device:
1033            logging.error('Device not found')
1034            return False
1035        return self._is_connected(device)
1036
1037
1038    @xmlrpc_server.dbus_safe(False)
1039    def disconnect_device(self, address):
1040        """Disconnects a device.
1041
1042        Disconnects a device if it is connected.
1043
1044        @param address: Address of the device to disconnect.
1045
1046        @returns: True on success. False otherwise.
1047
1048        """
1049        device = self._find_device(address)
1050        if not device:
1051            logging.error('Device not found')
1052            return False
1053        if not self._is_connected(device):
1054          logging.info('Device is not connected')
1055          return True
1056        device.Disconnect()
1057        return not self._is_connected(device)
1058
1059
1060    def btmon_start(self):
1061        """Start btmon monitoring."""
1062        self.btmon.start()
1063
1064
1065    def btmon_stop(self):
1066        """Stop btmon monitoring."""
1067        self.btmon.stop()
1068
1069
1070    def btmon_get(self, search_str, start_str):
1071        """Get btmon output contents.
1072
1073        @param search_str: only lines with search_str would be kept.
1074        @param start_str: all lines before the occurrence of start_str would be
1075                filtered.
1076
1077        @returns: the recorded btmon output.
1078
1079        """
1080        return self.btmon.get_contents(search_str=search_str,
1081                                       start_str=start_str)
1082
1083
1084    def btmon_find(self, pattern_str):
1085        """Find if a pattern string exists in btmon output.
1086
1087        @param pattern_str: the pattern string to find.
1088
1089        @returns: True on success. False otherwise.
1090
1091        """
1092        return self.btmon.find(pattern_str)
1093
1094
1095    @xmlrpc_server.dbus_safe(False)
1096    def advertising_async_method(self, dbus_method,
1097                                 reply_handler, error_handler, *args):
1098        """Run an async dbus method.
1099
1100        @param dbus_method: the dbus async method to invoke.
1101        @param reply_handler: the reply handler for the dbus method.
1102        @param error_handler: the error handler for the dbus method.
1103        @param *args: additional arguments for the dbus method.
1104
1105        @returns: an empty string '' on success;
1106                  None if there is no _advertising interface manager; and
1107                  an error string if the dbus method fails.
1108
1109        """
1110
1111        def successful_cb():
1112            """Called when the dbus_method completed successfully."""
1113            reply_handler()
1114            self.advertising_cb_msg = ''
1115            self._adv_mainloop.quit()
1116
1117
1118        def error_cb(error):
1119            """Called when the dbus_method failed."""
1120            error_handler(error)
1121            self.advertising_cb_msg = str(error)
1122            self._adv_mainloop.quit()
1123
1124
1125        if not self._advertising:
1126            return None
1127
1128        # Call dbus_method with handlers.
1129        dbus_method(*args, reply_handler=successful_cb, error_handler=error_cb)
1130
1131        self._adv_mainloop.run()
1132
1133        return self.advertising_cb_msg
1134
1135
1136    def register_advertisement(self, advertisement_data):
1137        """Register an advertisement.
1138
1139        Note that rpc supports only conformable types. Hence, a
1140        dict about the advertisement is passed as a parameter such
1141        that the advertisement object could be constructed on the host.
1142
1143        @param advertisement_data: a dict of the advertisement to register.
1144
1145        @returns: True on success. False otherwise.
1146
1147        """
1148        adv = advertisement.Advertisement(self._system_bus, advertisement_data)
1149        self.advertisements.append(adv)
1150        return self.advertising_async_method(
1151                self._advertising.RegisterAdvertisement,
1152                # reply handler
1153                lambda: logging.info('register_advertisement: succeeded.'),
1154                # error handler
1155                lambda error: logging.error(
1156                    'register_advertisement: failed: %s', str(error)),
1157                # other arguments
1158                adv.get_path(), {})
1159
1160
1161    def unregister_advertisement(self, advertisement_data):
1162        """Unregister an advertisement.
1163
1164        Note that to unregister an advertisement, it is required to use
1165        the same self._advertising interface manager. This is because
1166        bluez only allows the same sender to invoke UnregisterAdvertisement
1167        method. Hence, watch out that the bluetoothd is not restarted or
1168        self.start_bluetoothd() is not executed between the time span that
1169        an advertisement is registered and unregistered.
1170
1171        @param advertisement_data: a dict of the advertisements to unregister.
1172
1173        @returns: True on success. False otherwise.
1174
1175        """
1176        path = advertisement_data.get('Path')
1177        for index, adv in enumerate(self.advertisements):
1178            if adv.get_path() == path:
1179                break
1180        else:
1181            logging.error('Fail to find the advertisement under the path: %s',
1182                          path)
1183            return False
1184
1185        result = self.advertising_async_method(
1186                self._advertising.UnregisterAdvertisement,
1187                # reply handler
1188                lambda: logging.info('unregister_advertisement: succeeded.'),
1189                # error handler
1190                lambda error: logging.error(
1191                    'unregister_advertisement: failed: %s', str(error)),
1192                # other arguments
1193                adv.get_path())
1194
1195        # Call remove_from_connection() so that the same path could be reused.
1196        adv.remove_from_connection()
1197        del self.advertisements[index]
1198
1199        return result
1200
1201
1202    def set_advertising_intervals(self, min_adv_interval_ms,
1203                                  max_adv_interval_ms):
1204        """Set advertising intervals.
1205
1206        @param min_adv_interval_ms: the min advertising interval in ms.
1207        @param max_adv_interval_ms: the max advertising interval in ms.
1208
1209        @returns: True on success. False otherwise.
1210
1211        """
1212        return self.advertising_async_method(
1213                self._advertising.SetAdvertisingIntervals,
1214                # reply handler
1215                lambda: logging.info('set_advertising_intervals: succeeded.'),
1216                # error handler
1217                lambda error: logging.error(
1218                    'set_advertising_intervals: failed: %s', str(error)),
1219                # other arguments
1220                min_adv_interval_ms, max_adv_interval_ms)
1221
1222
1223    def reset_advertising(self):
1224        """Reset advertising.
1225
1226        This includes un-registering all advertisements, reset advertising
1227        intervals, and disable advertising.
1228
1229        @returns: True on success. False otherwise.
1230
1231        """
1232        # It is required to execute remove_from_connection() to unregister the
1233        # object-path handler of each advertisement. In this way, we could
1234        # register an advertisement with the same path repeatedly.
1235        for adv in self.advertisements:
1236            adv.remove_from_connection()
1237        del self.advertisements[:]
1238
1239        return self.advertising_async_method(
1240                self._advertising.ResetAdvertising,
1241                # reply handler
1242                lambda: logging.info('reset_advertising: succeeded.'),
1243                # error handler
1244                lambda error: logging.error(
1245                    'reset_advertising: failed: %s', str(error)))
1246
1247
1248if __name__ == '__main__':
1249    logging.basicConfig(level=logging.DEBUG)
1250    handler = logging.handlers.SysLogHandler(address='/dev/log')
1251    formatter = logging.Formatter(
1252            'bluetooth_device_xmlrpc_server: [%(levelname)s] %(message)s')
1253    handler.setFormatter(formatter)
1254    logging.getLogger().addHandler(handler)
1255    logging.debug('bluetooth_device_xmlrpc_server main...')
1256    server = xmlrpc_server.XmlRpcServer(
1257            'localhost',
1258            constants.BLUETOOTH_DEVICE_XMLRPC_SERVER_PORT)
1259    server.register_delegate(BluetoothDeviceXmlRpcDelegate())
1260    server.run()
1261