autotest: Adds GATT Read/write support.

BUG=b:29448871
TEST=Sample jetstream test which reads/writes using
GATT, using samus as the peripheral.

Change-Id: I200fa125cd367f70c8de76f96f2b7b7f752e6041
Reviewed-on: https://chromium-review.googlesource.com/550208
Commit-Ready: Dane Pollock <danepollock@google.com>
Tested-by: Dane Pollock <danepollock@google.com>
Reviewed-by: Shyh-In Hwang <josephsih@chromium.org>
diff --git a/client/cros/bluetooth/bluetooth_device_xmlrpc_server.py b/client/cros/bluetooth/bluetooth_device_xmlrpc_server.py
index 9a562ee..d2918e7 100755
--- a/client/cros/bluetooth/bluetooth_device_xmlrpc_server.py
+++ b/client/cros/bluetooth/bluetooth_device_xmlrpc_server.py
@@ -4,6 +4,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import base64
 import dbus
 import dbus.mainloop.glib
 import dbus.service
@@ -13,7 +14,6 @@
 import logging.handlers
 import os
 import shutil
-import time
 
 import common
 from autotest_lib.client.bin import utils
@@ -24,6 +24,20 @@
 from autotest_lib.client.cros.bluetooth import output_recorder
 
 
+def _dbus_byte_array_to_b64_string(dbus_byte_array):
+    """Base64 encodes a dbus byte array for use with the xml rpc proxy."""
+    return base64.standard_b64encode(bytearray(dbus_byte_array))
+
+
+def _b64_string_to_dbus_byte_array(b64_string):
+  """Base64 decodes a dbus byte array for use with the xml rpc proxy."""
+  dbus_array = dbus.Array([], signature=dbus.Signature('y'))
+  bytes = bytearray(base64.standard_b64decode(b64_string))
+  for byte in bytes:
+    dbus_array.append(dbus.Byte(byte))
+  return dbus_array
+
+
 class PairingAgent(dbus.service.Object):
     """The agent handling the authentication process of bluetooth pairing.
 
@@ -85,6 +99,7 @@
     BLUEZ_MANAGER_IFACE = 'org.freedesktop.DBus.ObjectManager'
     BLUEZ_ADAPTER_IFACE = 'org.bluez.Adapter1'
     BLUEZ_DEVICE_IFACE = 'org.bluez.Device1'
+    BLUEZ_GATT_IFACE = 'org.bluez.GattCharacteristic1'
     BLUEZ_LE_ADVERTISING_MANAGER_IFACE = 'org.bluez.LEAdvertisingManager1'
     BLUEZ_AGENT_MANAGER_PATH = '/org/bluez'
     BLUEZ_AGENT_MANAGER_IFACE = 'org.bluez.AgentManager1'
@@ -753,6 +768,27 @@
 
         @returns: An 'org.bluez.Device1' interface to the device.
                   None if device can not be found.
+        """
+        path = self._get_device_path(address)
+        if path:
+            obj = self._system_bus.get_object(
+                        self.BLUEZ_SERVICE_NAME, path)
+            return dbus.Interface(obj, self.BLUEZ_DEVICE_IFACE)
+        logging.info('Device not found')
+        return None
+
+
+    @xmlrpc_server.dbus_safe(False)
+    def _get_device_path(self, address):
+        """Gets the path for a device with a given address.
+
+        Find the device with a given address and returns the
+        the path for the device.
+
+        @param address: Address of the device.
+
+        @returns: The path to the address of the device, or None if device is
+            not found in the object tree.
 
         """
         objects = self._bluez.GetManagedObjects(
@@ -763,16 +799,13 @@
                 continue
             if (device['Address'] == address and
                 path.startswith(self._adapter.object_path)):
-                obj = self._system_bus.get_object(
-                        self.BLUEZ_SERVICE_NAME, path)
-                return dbus.Interface(obj, self.BLUEZ_DEVICE_IFACE)
-        logging.info('Device not found')
-        return None
+                return path
+        logging.info('Device path not found')
 
 
     @xmlrpc_server.dbus_safe(False)
     def _setup_pairing_agent(self, pin):
-        """Initializes and resiters a PairingAgent to handle authenticaiton.
+        """Initializes and resiters a PairingAgent to handle authentication.
 
         @param pin: The pin code this agent will answer.
 
@@ -831,7 +864,7 @@
 
 
     @xmlrpc_server.dbus_safe(False)
-    def _is_connected(self,  device):
+    def _is_connected(self, device):
         """Checks if a device is connected.
 
         @param device: An 'org.bluez.Device1' interface to the device.
@@ -845,6 +878,7 @@
         return bool(connected)
 
 
+
     @xmlrpc_server.dbus_safe(False)
     def _set_trusted_by_device(self, device, trusted=True):
         """Set the device trusted by device object.
@@ -936,7 +970,7 @@
             return True
 
         device_path = device.object_path
-        logging.info('Device %s is found.' % device.object_path)
+        logging.info('Device %s is found.', device.object_path)
 
         self._setup_pairing_agent(pin)
         mainloop = gobject.MainLoop()
@@ -1057,6 +1091,45 @@
         return not self._is_connected(device)
 
 
+    @xmlrpc_server.dbus_safe(False)
+    def _device_services_resolved(self, device):
+        """Checks if services are resolved.
+
+        @param device: An 'org.bluez.Device1' interface to the device.
+
+        @returns: True if device is connected. False otherwise.
+
+        """
+        logging.info('device for services resolved: %s', device)
+        props = dbus.Interface(device, dbus.PROPERTIES_IFACE)
+        resolved = props.Get(self.BLUEZ_DEVICE_IFACE, 'ServicesResolved')
+        logging.info('Services resolved = %r', resolved)
+        return bool(resolved)
+
+
+    @xmlrpc_server.dbus_safe(False)
+    def device_services_resolved(self, address):
+        """Checks if service discovery is complete on a device.
+
+        Checks whether service discovery has been completed..
+
+        @param address: Address of the remote device.
+
+        @returns: True on success. False otherwise.
+
+        """
+        device = self._find_device(address)
+        if not device:
+            logging.error('Device not found')
+            return False
+
+        if not self._is_connected(device):
+          logging.info('Device is not connected')
+          return False
+
+        return self._device_services_resolved(device)
+
+
     def btmon_start(self):
         """Start btmon monitoring."""
         self.btmon.start()
@@ -1245,6 +1318,123 @@
                     'reset_advertising: failed: %s', str(error)))
 
 
+    @xmlrpc_server.dbus_safe(False)
+    def get_characteristic_map(self, address):
+        """Gets a map of characteristic paths for a device.
+
+        Walks the object tree, and returns a map of uuids to object paths for
+        all resolved gatt characteristics.
+
+        @param address: The MAC address of the device to retrieve
+            gatt characteristic uuids and paths from.
+
+        @returns: A dictionary of characteristic paths, keyed by uuid.
+
+        """
+        device_path = self._get_device_path(address)
+        char_map = {}
+
+        if device_path:
+          objects = self._bluez.GetManagedObjects(
+              dbus_interface=self.BLUEZ_MANAGER_IFACE, byte_arrays=False)
+
+          for path, ifaces in objects.iteritems():
+              if (self.BLUEZ_GATT_IFACE in ifaces and
+                  path.startswith(device_path)):
+                  uuid = ifaces[self.BLUEZ_GATT_IFACE]['UUID'].lower()
+                  char_map[uuid] = path
+        else:
+            logging.warning('Device %s not in object tree.', address)
+
+        return char_map
+
+
+    @xmlrpc_server.dbus_safe(False)
+    def _get_char_object(self, uuid, address):
+        """Gets a characteristic object.
+
+        Gets a characteristic object for a given uuid and address.
+
+        @param uuid: The uuid of the characteristic, as a string.
+        @param address: The MAC address of the remote device.
+
+        @returns: A dbus interface for the characteristic if the uuid/address
+                      is in the object tree.
+                  None if the address/uuid is not found in the object tree.
+
+        """
+        path = self.get_characteristic_map(address).get(uuid)
+        if not path:
+            return None
+        return dbus.Interface(
+            self._system_bus.get_object(self.BLUEZ_SERVICE_NAME, path),
+            self.BLUEZ_GATT_IFACE)
+
+
+    @xmlrpc_server.dbus_safe(None)
+    def read_characteristic(self, uuid, address):
+        """Reads the value of a gatt characteristic.
+
+        Reads the current value of a gatt characteristic. Base64 endcoding is
+        used for compatibility with the XML RPC interface.
+
+        @param uuid: The uuid of the characteristic to read, as a string.
+        @param address: The MAC address of the remote device.
+
+        @returns: A b64 encoded version of a byte array containing the value
+                      if the uuid/address is in the object tree.
+                  None if the uuid/address was not found in the object tree, or
+                      if a DBus exception was raised by the read operation.
+
+        """
+        char_obj = self._get_char_object(uuid, address)
+        if char_obj is None:
+            return None
+        value = char_obj.ReadValue(dbus.Dictionary())
+        return _dbus_byte_array_to_b64_string(value)
+
+
+    @xmlrpc_server.dbus_safe(None)
+    def write_characteristic(self, uuid, address, value):
+        """Performs a write operation on a gatt characteristic.
+
+        Writes to a GATT characteristic on a remote device. Base64 endcoding is
+        used for compatibility with the XML RPC interface.
+
+        @param uuid: The uuid of the characteristic to write to, as a string.
+        @param address: The MAC address of the remote device, as a string.
+        @param value: A byte array containing the data to write.
+
+        @returns: True if the write operation does not raise an exception.
+                  None if the uuid/address was not found in the object tree, or
+                      if a DBus exception was raised by the write operation.
+
+        """
+        char_obj = self._get_char_object(uuid, address)
+        if char_obj is None:
+            return None
+        dbus_value = _b64_string_to_dbus_byte_array(value)
+        char_obj.WriteValue(dbus_value, dbus.Dictionary())
+        return True
+
+
+    @xmlrpc_server.dbus_safe(False)
+    def is_characteristic_path_resolved(self, uuid, address):
+        """Checks whether a characteristic is in the object tree.
+
+        Checks whether a characteristic is curently found in the object tree.
+
+        @param uuid: The uuid of the characteristic to search for.
+        @param address: The MAC address of the device on which to search for
+            the characteristic.
+
+        @returns: True if the characteristic is found.
+                  False if the characteristic path is not found.
+
+        """
+        return bool(self.get_characteristic_map(address).get(uuid))
+
+
 if __name__ == '__main__':
     logging.basicConfig(level=logging.DEBUG)
     handler = logging.handlers.SysLogHandler(address='/dev/log')
diff --git a/server/cros/bluetooth/bluetooth_device.py b/server/cros/bluetooth/bluetooth_device.py
index b0d9a5f..ae18085 100644
--- a/server/cros/bluetooth/bluetooth_device.py
+++ b/server/cros/bluetooth/bluetooth_device.py
@@ -2,6 +2,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import base64
 import json
 
 from autotest_lib.client.cros import constants
@@ -456,6 +457,17 @@
         return self._proxy.device_is_paired(address)
 
 
+    def device_services_resolved(self, address):
+        """Checks if services are resolved for a device.
+
+        @param address: address of the device.
+
+        @returns: True if services are resolved. False otherwise.
+
+        """
+        return self._proxy.device_services_resolved(address)
+
+
     def set_trusted(self, address, trusted=True):
         """Set the device trusted.
 
@@ -623,6 +635,59 @@
         return self._proxy.reset_advertising()
 
 
+    def read_characteristic(self, uuid, address):
+        """Reads the value of a gatt characteristic.
+
+        Reads the current value of a gatt characteristic.
+
+        @param uuid: The uuid of the characteristic to read, as a string.
+        @param address: The MAC address of the remote device.
+
+        @returns: A byte array containing the value of the if the uuid/address
+                      was found in the object tree.
+                  None if the uuid/address was not found in the object tree, or
+                      if a DBus exception was raised by the read operation.
+
+        """
+        value = self._proxy.read_characteristic(uuid, address)
+        if value is None:
+            return None
+        return bytearray(base64.standard_b64decode(value))
+
+
+    def write_characteristic(self, uuid, address, bytes_to_write):
+        """Performs a write operation on a gatt characteristic.
+
+        Writes to a GATT characteristic on a remote device.
+
+        @param uuid: The uuid of the characteristic to write to, as a string.
+        @param address: The MAC address of the remote device, as a string.
+        @param bytes_to_write: A byte array containing the data to write.
+
+        @returns: True if the write operation does not raise an exception.
+                  None if the uuid/address was not found in the object tree, or
+                      if a DBus exception was raised by the write operation.
+
+        """
+        return self._proxy.write_characteristic(
+            uuid, address, base64.standard_b64encode(bytes_to_write))
+
+
+    def is_characteristic_path_resolved(self, uuid, address):
+        """Checks whether a characteristic is in the object tree.
+
+        Checks whether a characteristic is curently found in the object tree.
+
+        @param uuid: The uuid of the characteristic to search for.
+        @param address: The MAC address of the device on which to search for
+            the characteristic.
+
+        @returns: True if the characteristic is found, False otherwise.
+
+        """
+        return self._proxy.is_characteristic_path_resolved(uuid, address)
+
+
     def copy_logs(self, destination):
         """Copy the logs generated by this device to a given location.
 
@@ -632,11 +697,22 @@
         self.host.collect_logs(self.XMLRPC_LOG_PATH, destination)
 
 
-    def close(self):
-        """Tear down state associated with the client."""
+    def close(self, close_host=True):
+        """Tear down state associated with the client.
+
+        @param close_host: If True, shut down the xml rpc server by closing the
+            underlying host object (which also shuts down all other xml rpc
+            servers running on the DUT). Otherwise, only shut down the
+            bluetooth device xml rpc server, which can be desirable if the host
+            object and/or other xml rpc servers need to be used afterwards.
+        """
         # Turn off the discoverable flag since it may affect future tests.
         self._proxy.set_discoverable(False)
         # Leave the adapter powered off, but don't do a full reset.
         self._proxy.set_powered(False)
         # This kills the RPC server.
-        self.host.close()
+        if close_host:
+          self.host.close()
+        else:
+          self.host.rpc_server_tracker.disconnect(
+              constants.BLUETOOTH_DEVICE_XMLRPC_SERVER_PORT)