bluetooth: Add battery reporting test

This adds basic battery reporting test to test that battery reporting
via GATT BAS is received by BlueZ and exposed through org.bluez.Battery1
interface.

BUG=b:166319884
TEST=Ran test_that bluetooth_AdapterLEHealth.battery_reporting

Change-Id: Iea5886cf73224183b60dff5bf3cd9f99960d9399
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/autotest/+/2561558
Reviewed-by: Abhishek Pandit-Subedi <abhishekpandit@chromium.org>
Reviewed-by: Daniel Winkler <danielwinkler@google.com>
Tested-by: Sonny Sasaka <sonnysasaka@chromium.org>
Commit-Queue: Sonny Sasaka <sonnysasaka@chromium.org>
diff --git a/client/cros/multimedia/bluetooth_facade_native.py b/client/cros/multimedia/bluetooth_facade_native.py
index 334ac66..4cce4a9 100644
--- a/client/cros/multimedia/bluetooth_facade_native.py
+++ b/client/cros/multimedia/bluetooth_facade_native.py
@@ -344,6 +344,7 @@
     BLUEZ_DEBUG_LOG_IFACE = 'org.chromium.Bluetooth.Debug'
     BLUEZ_MANAGER_IFACE = 'org.freedesktop.DBus.ObjectManager'
     BLUEZ_ADAPTER_IFACE = 'org.bluez.Adapter1'
+    BLUEZ_BATTERY_IFACE = 'org.bluez.Battery1'
     BLUEZ_DEVICE_IFACE = 'org.bluez.Device1'
     BLUEZ_GATT_SERV_IFACE = 'org.bluez.GattService1'
     BLUEZ_GATT_CHAR_IFACE = 'org.bluez.GattCharacteristic1'
@@ -1478,6 +1479,29 @@
 
         return self._encode_base64_json(prop_val)
 
+    @xmlrpc_server.dbus_safe(None)
+    def get_battery_property(self, address, prop_name):
+        """Read a property from Battery1 interface.
+
+        @param address: Address of the device to query
+        @param prop_name: Property to be queried
+
+        @return The battery percentage value, or None if does not exist.
+        """
+
+        prop_val = None
+
+        # Grab dbus object, _find_battery will catch any thrown dbus error
+        battery_obj = self._find_battery(address)
+
+        if battery_obj:
+            # Query dbus object for property
+            prop_val = battery_obj.Get(self.BLUEZ_BATTERY_IFACE,
+                                       prop_name,
+                                       dbus_interface=dbus.PROPERTIES_IFACE)
+
+        return dbus_util.dbus2primitive(prop_val)
+
     @xmlrpc_server.dbus_safe(False)
     def set_discovery_filter(self, filter):
         """Set the discovery filter.
@@ -1670,6 +1694,25 @@
         logging.info('Device not found')
         return None
 
+    @xmlrpc_server.dbus_safe(None)
+    def _find_battery(self, address):
+        """Finds the battery with a given address.
+
+        Find the battery with a given address and returns the
+        battery interface.
+
+        @param address: Address of the device.
+
+        @returns: An 'org.bluez.Battery1' 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_BATTERY_IFACE)
+        logging.info('Battery 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.
diff --git a/server/cros/bluetooth/bluetooth_adapter_hidreports_tests.py b/server/cros/bluetooth/bluetooth_adapter_hidreports_tests.py
index e8aca7e..1999889 100644
--- a/server/cros/bluetooth/bluetooth_adapter_hidreports_tests.py
+++ b/server/cros/bluetooth/bluetooth_adapter_hidreports_tests.py
@@ -51,6 +51,15 @@
         self.test_keyboard_input_from_trace(device, "simple_text")
 
 
+    def run_battery_reporting_tests(self, device):
+        """Run battery reporting tests.
+
+        @param device: the Bluetooth device.
+
+        """
+
+        self.test_battery_reporting(device)
+
     def run_hid_reports_test(self, device,
                              check_connected_method=lambda device: True,
                              suspend_resume=False, reboot=False):
diff --git a/server/cros/bluetooth/bluetooth_adapter_tests.py b/server/cros/bluetooth/bluetooth_adapter_tests.py
index 48547e1..9edc326 100644
--- a/server/cros/bluetooth/bluetooth_adapter_tests.py
+++ b/server/cros/bluetooth/bluetooth_adapter_tests.py
@@ -4279,6 +4279,21 @@
         return all(self.results.values())
 
 
+    @test_retry_and_log
+    def test_battery_reporting(self, device):
+        """ Tests that battery reporting through GATT can be received
+
+        @param device: the meta device containing a Bluetooth device
+
+        @returns: true if battery reporting is received
+        """
+
+        percentage = self.bluetooth_facade.get_battery_property(
+                device.address, 'Percentage')
+
+        return percentage > 0
+
+
     # -------------------------------------------------------------------
     # Autotest methods
     # -------------------------------------------------------------------
diff --git a/server/cros/bluetooth/bluetooth_device.py b/server/cros/bluetooth/bluetooth_device.py
index 83db633..9a0fd4c 100644
--- a/server/cros/bluetooth/bluetooth_device.py
+++ b/server/cros/bluetooth/bluetooth_device.py
@@ -594,6 +594,19 @@
 
 
     @proxy_thread_safe
+    def get_battery_property(self, address, prop_name):
+        """Read a property of battery by directly querying the dbus object
+
+        @param address: Address of the device to query
+        @param prop_name: Property to be queried
+
+        @return The property if battery is found and has property,
+          None otherwise
+        """
+
+        return self._proxy.get_battery_property(address, prop_name)
+
+    @proxy_thread_safe
     def start_discovery(self):
         """Start discovery of remote devices.
 
diff --git a/server/site_tests/bluetooth_AdapterLEHealth/bluetooth_AdapterLEHealth.py b/server/site_tests/bluetooth_AdapterLEHealth/bluetooth_AdapterLEHealth.py
index 9f0f1a1..cae17f0 100644
--- a/server/site_tests/bluetooth_AdapterLEHealth/bluetooth_AdapterLEHealth.py
+++ b/server/site_tests/bluetooth_AdapterLEHealth/bluetooth_AdapterLEHealth.py
@@ -95,6 +95,18 @@
         self.run_keyboard_tests(device=device)
 
 
+    @test_wrapper('Battery Reporting', devices={'BLE_MOUSE': 1})
+    def battery_reporting(self):
+        """Run battery reporting tests"""
+
+        device = self.devices['BLE_MOUSE'][0]
+        # Let the adapter pair, and connect to the target device.
+        self.assert_on_fail(self.test_discover_device(device.address))
+        self.assert_on_fail(
+                self.test_pairing(device.address, device.pin, trusted=True))
+
+        self.run_battery_reporting_tests(device=device)
+
     @test_wrapper('Auto Reconnect', devices={'BLE_MOUSE':1})
     def le_auto_reconnect(self):
         """LE reconnection loop by reseting HID and check reconnection"""
diff --git a/server/site_tests/bluetooth_AdapterLEHealth/control.battery_reporting b/server/site_tests/bluetooth_AdapterLEHealth/control.battery_reporting
new file mode 100644
index 0000000..595dc53
--- /dev/null
+++ b/server/site_tests/bluetooth_AdapterLEHealth/control.battery_reporting
@@ -0,0 +1,32 @@
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from autotest_lib.server import utils
+
+AUTHOR = 'chromeos-bluetooth'
+NAME = 'bluetooth_AdapterLEHealth.battery_reporting'
+PURPOSE = ('Test the GATT Battery Service profile')
+CRITERIA = 'Pass all health test'
+ATTRIBUTES = 'suite:bluetooth_flaky'
+TIME = 'MEDIUM'
+TEST_CATEGORY = 'Functional'
+TEST_CLASS = 'bluetooth'
+TEST_TYPE = 'server'
+DEPENDENCIES = 'bluetooth, working_bluetooth_btpeer:1'
+
+DOC = """
+
+     Server side bluetooth tests about receiving battery reports via GATT.
+
+    """
+
+args_dict = utils.args_to_dict(args)
+
+def run(machine):
+    host = hosts.create_host(machine)
+    job.run_test('bluetooth_AdapterLEHealth', host=host,
+                 num_iterations=1, args_dict=args_dict,
+                 test_name=NAME.split('.')[1])
+
+parallel_simple(run, machines)