Create a smarter device selector for BT tests

Work towards selecting Android devices to test
against via using their set feature properties
as well as the supported BT UUIDS on the device.

Also add the ability to selected prefered device
order for tests that care about order and do not
know about device capabilties.

Bug: 110536180
Test: Manual execution
Change-Id: Ia72403893720bbdd1e8699f04e2ce0439bb94b90
(cherry picked from commit 9bce15d9087a80c2bf565717e9b61c1a2fcd02e3)
diff --git a/acts/framework/acts/test_utils/bt/BluetoothBaseTest.py b/acts/framework/acts/test_utils/bt/BluetoothBaseTest.py
index cd82974..21d63c7 100644
--- a/acts/framework/acts/test_utils/bt/BluetoothBaseTest.py
+++ b/acts/framework/acts/test_utils/bt/BluetoothBaseTest.py
@@ -31,6 +31,7 @@
 from acts.libs.proto.proto_utils import compile_import_proto
 from acts.libs.proto.proto_utils import parse_proto_to_ascii
 from acts.test_utils.bt.bt_metrics_utils import get_bluetooth_metrics
+from acts.test_utils.bt.bt_test_utils import get_device_selector_dictionary
 from acts.test_utils.bt.bt_test_utils import reset_bluetooth
 from acts.test_utils.bt.bt_test_utils import setup_multiple_devices_for_bt_test
 from acts.test_utils.bt.bt_test_utils import take_btsnoop_logs
@@ -52,6 +53,13 @@
         BaseTestClass.__init__(self, controllers)
         for ad in self.android_devices:
             self._setup_bt_libs(ad)
+        if 'preferred_device_order' in self.user_params:
+            prefered_device_order = self.user_params['preferred_device_order']
+            for i, ad in enumerate(self.android_devices):
+                if ad.serial in prefered_device_order:
+                    index = prefered_device_order.index(ad.serial)
+                    self.android_devices[i], self.android_devices[index] = \
+                        self.android_devices[index], self.android_devices[i]
 
     def collect_bluetooth_manager_metrics_logs(self, ads, test_name):
         """
@@ -123,6 +131,8 @@
         return _safe_wrap_test_case
 
     def setup_class(self):
+        self.device_selector = get_device_selector_dictionary(
+            self.android_devices)
         if "reboot_between_test_class" in self.user_params:
             threads = []
             for a in self.android_devices:
@@ -235,3 +245,6 @@
         # Shell command library
         setattr(android_device, "shell",
                 ShellCommands(log=self.log, dut=android_device))
+        # Setup Android Device feature list
+        setattr(android_device, "features",
+                android_device.adb.shell("pm list features").split("\n"))
diff --git a/acts/framework/acts/test_utils/bt/bt_test_utils.py b/acts/framework/acts/test_utils/bt/bt_test_utils.py
index 9a930ab..521b144 100644
--- a/acts/framework/acts/test_utils/bt/bt_test_utils.py
+++ b/acts/framework/acts/test_utils/bt/bt_test_utils.py
@@ -223,7 +223,8 @@
     threads = []
     try:
         for a in android_devices:
-            thread = threading.Thread(target=factory_reset_bluetooth, args=([[a]]))
+            thread = threading.Thread(
+                target=factory_reset_bluetooth, args=([[a]]))
             threads.append(thread)
             thread.start()
         for t in threads:
@@ -282,7 +283,11 @@
             return False
     return True
 
-def wait_for_bluetooth_manager_state(droid, state=None, timeout=10, threshold=5):
+
+def wait_for_bluetooth_manager_state(droid,
+                                     state=None,
+                                     timeout=10,
+                                     threshold=5):
     """ Waits for BlueTooth normalized state or normalized explicit state
     args:
         droid: droid device object
@@ -301,7 +306,8 @@
             # for any normalized state
             if state is None:
                 if len(set(all_states[-threshold:])) == 1:
-                    log.info("State normalized {}".format(set(all_states[-threshold:])))
+                    log.info("State normalized {}".format(
+                        set(all_states[-threshold:])))
                     return True
             else:
                 # explicit check against normalized state
@@ -310,9 +316,11 @@
         time.sleep(0.5)
     log.error(
         "Bluetooth state fails to normalize" if state is None else
-        "Failed to match bluetooth state, current state {} expected state {}".format(get_state(), state))
+        "Failed to match bluetooth state, current state {} expected state {}".
+        format(get_state(), state))
     return False
 
+
 def factory_reset_bluetooth(android_devices):
     """Clears Bluetooth stack of input Android device list.
 
@@ -340,6 +348,7 @@
             return False
     return True
 
+
 def reset_bluetooth(android_devices):
     """Resets Bluetooth state of input Android device list.
 
@@ -576,6 +585,7 @@
     mac_address = event['data']['Result']['deviceInfo']['address']
     return mac_address, advertise_callback, scan_callback
 
+
 def enable_bluetooth(droid, ed):
     if droid.bluetoothCheckState() is True:
         return True
@@ -594,6 +604,7 @@
 
     return True
 
+
 def disable_bluetooth(droid):
     """Disable Bluetooth on input Droid object.
 
@@ -1000,7 +1011,8 @@
                 bluetooth_profile_connection_state_changed, bt_default_timeout)
             pri_ad.log.info("Got event {}".format(profile_event))
         except Exception as e:
-            pri_ad.log.error("Did not disconnect from Profiles. Reason {}".format(e))
+            pri_ad.log.error(
+                "Did not disconnect from Profiles. Reason {}".format(e))
             return False
 
         profile = profile_event['data']['profile']
@@ -1387,3 +1399,78 @@
         if (device["address"] == source.droid.bluetoothGetLocalAddress()):
             return True
     return False
+
+
+def get_device_selector_dictionary(android_device_list):
+    """Create a dictionary of Bluetooth features vs Android devices.
+
+    Args:
+        android_device_list: The list of Android devices.
+    Returns:
+        A dictionary of profiles/features to Android devices.
+    """
+    selector_dict = {}
+    for ad in android_device_list:
+        uuids = ad.droid.bluetoothGetLocalUuids()
+
+        for profile, uuid_const in sig_uuid_constants.items():
+            uuid_check = sig_uuid_constants['BASE_UUID'].format(
+                uuid_const).lower()
+            if uuid_check in uuids:
+                if profile in selector_dict:
+                    selector_dict[profile].append(ad)
+                else:
+                    selector_dict[profile] = [ad]
+
+        # Various services may not be active during BT startup.
+        # If the device can be identified through adb shell pm list features
+        # then try to add them to the appropriate profiles / features.
+
+        # Android TV.
+        if "feature:com.google.android.tv.installed" in ad.features:
+            ad.log.info("Android TV device found.")
+            supported_profiles = ['AudioSink']
+            _add_android_device_to_dictionary(ad, supported_profiles,
+                                              selector_dict)
+
+        # Android Auto
+        elif "feature:android.hardware.type.automotive" in ad.features:
+            ad.log.info("Android Auto device found.")
+            # Add: AudioSink , A/V_RemoteControl,
+            supported_profiles = [
+                'AudioSink', 'A/V_RemoteControl', 'Message Notification Server'
+            ]
+            _add_android_device_to_dictionary(ad, supported_profiles,
+                                              selector_dict)
+        # Android Wear
+        elif "feature:android.hardware.type.watch" in ad.features:
+            ad.log.info("Android Wear device found.")
+            supported_profiles = []
+            _add_android_device_to_dictionary(ad, supported_profiles,
+                                              selector_dict)
+        # Android Phone
+        elif "feature:android.hardware.telephony" in ad.features:
+            ad.log.info("Android Phone device found.")
+            # Add: AudioSink
+            supported_profiles = [
+                'AudioSource', 'A/V_RemoteControlTarget',
+                'Message Access Server'
+            ]
+            _add_android_device_to_dictionary(ad, supported_profiles,
+                                              selector_dict)
+    return selector_dict
+
+
+def _add_android_device_to_dictionary(android_device, profile_list,
+                                      selector_dict):
+    """Adds the AndroidDevice and supported features to the selector dictionary
+
+    Args:
+        android_device: The Android device.
+        profile_list: The list of profiles the Android device supports.
+    """
+    for profile in profile_list:
+        if profile in selector_dict and android_device not in selector_dict[profile]:
+            selector_dict[profile].append(android_device)
+        else:
+            selector_dict[profile] = [android_device]