1#!/usr/bin/env python3
2#
3# Copyright (C) 2016 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may not
6# use this file except in compliance with the License. You may obtain a copy of
7# the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations under
15# the License.
16"""
17Test script to automate the Bluetooth Audio Funhaus.
18"""
19from acts.keys import Config
20from acts.test_utils.bt.BtMetricsBaseTest import BtMetricsBaseTest
21from acts.test_utils.bt.bt_test_utils import bluetooth_enabled_check
22from acts.utils import bypass_setup_wizard
23from acts.utils import exe_cmd
24from acts.utils import sync_device_time
25import json
26import time
27import os
28
29BT_CONF_PATH = "/data/misc/bluedroid/bt_config.conf"
30
31
32class BtFunhausBaseTest(BtMetricsBaseTest):
33    """
34    Base class for Bluetooth A2DP audio tests, this class is in charge of
35    pushing link key to Android device so that it could be paired with remote
36    A2DP device, pushing music to Android device, playing audio, monitoring
37    audio play, and stop playing audio
38    """
39    music_file_to_play = ""
40    device_fails_to_connect_list = []
41
42    def __init__(self, controllers):
43        BtMetricsBaseTest.__init__(self, controllers)
44        self.ad = self.android_devices[0]
45        self.dongle = self.relay_devices[0]
46
47    def _pair_devices(self):
48        self.ad.droid.bluetoothStartPairingHelper(False)
49        self.dongle.enter_pairing_mode()
50
51        self.ad.droid.bluetoothBond(self.dongle.mac_address)
52
53        end_time = time.time() + 20
54        self.ad.log.info("Verifying devices are bonded")
55        while time.time() < end_time:
56            bonded_devices = self.ad.droid.bluetoothGetBondedDevices()
57
58            for d in bonded_devices:
59                if d['address'] == self.dongle.mac_address:
60                    self.ad.log.info("Successfully bonded to device.")
61                    self.log.info("Bonded devices:\n{}".format(bonded_devices))
62                return True
63        self.ad.log.info("Failed to bond devices.")
64        return False
65
66    def setup_test(self):
67        super(BtFunhausBaseTest, self).setup_test()
68        self.dongle.setup()
69        tries = 5
70        # Since we are not concerned with pairing in this test, try 5 times.
71        while tries > 0:
72            if self._pair_devices():
73                return True
74            else:
75                tries -= 1
76        return False
77
78    def teardown_test(self):
79        super(BtFunhausBaseTest, self).teardown_test()
80        self.dongle.clean_up()
81        return True
82
83    def on_fail(self, test_name, begin_time):
84        self.dongle.clean_up()
85        self._collect_bluetooth_manager_dumpsys_logs(self.android_devices)
86        super(BtFunhausBaseTest, self).on_fail(test_name, begin_time)
87
88    def setup_class(self):
89        if not super(BtFunhausBaseTest, self).setup_class():
90            return False
91        for ad in self.android_devices:
92            sync_device_time(ad)
93            # Disable Bluetooth HCI Snoop Logs for audio tests
94            ad.adb.shell("setprop persist.bluetooth.btsnoopenable false")
95            if not bypass_setup_wizard(ad):
96                self.log.debug(
97                    "Failed to bypass setup wizard, continuing test.")
98                # Add music to the Android device
99        return self._add_music_to_android_device(ad)
100
101    def _add_music_to_android_device(self, ad):
102        """
103        Add music to Android device as specified by the test config
104        :param ad: Android device
105        :return: True on success, False on failure
106        """
107        self.log.info("Pushing music to the Android device.")
108        music_path_str = "bt_music"
109        android_music_path = "/sdcard/Music/"
110        if music_path_str not in self.user_params:
111            self.log.error("Need music for audio testcases...")
112            return False
113        music_path = self.user_params[music_path_str]
114        if type(music_path) is list:
115            self.log.info("Media ready to push as is.")
116        elif not os.path.isdir(music_path):
117            music_path = os.path.join(
118                self.user_params[Config.key_config_path.value], music_path)
119            if not os.path.isdir(music_path):
120                self.log.error(
121                    "Unable to find music directory {}.".format(music_path))
122                return False
123        if type(music_path) is list:
124            for item in music_path:
125                self.music_file_to_play = item
126                ad.adb.push("{} {}".format(item, android_music_path))
127        else:
128            for dirname, dirnames, filenames in os.walk(music_path):
129                for filename in filenames:
130                    self.music_file_to_play = filename
131                    file = os.path.join(dirname, filename)
132                    # TODO: Handle file paths with spaces
133                    ad.adb.push("{} {}".format(file, android_music_path))
134        ad.reboot()
135        return True
136
137    def _collect_bluetooth_manager_dumpsys_logs(self, ads):
138        """
139        Collect "adb shell dumpsys bluetooth_manager" logs
140        :param ads: list of active Android devices
141        :return: None
142        """
143        for ad in ads:
144            serial = ad.serial
145            out_name = "{}_{}".format(serial, "bluetooth_dumpsys.txt")
146            dumpsys_path = ''.join((ad.log_path, "/BluetoothDumpsys"))
147            os.makedirs(dumpsys_path, exist_ok=True)
148            cmd = ''.join(
149                ("adb -s ", serial, " shell dumpsys bluetooth_manager > ",
150                 dumpsys_path, "/", out_name))
151            exe_cmd(cmd)
152
153    def start_playing_music_on_all_devices(self):
154        """
155        Start playing music
156        :return: None
157        """
158        self.ad.droid.mediaPlayOpen("file:///sdcard/Music/{}".format(
159            self.music_file_to_play.split("/")[-1]))
160        self.ad.droid.mediaPlaySetLooping(True)
161        self.ad.log.info("Music is now playing.")
162
163    def monitor_music_play_util_deadline(self, end_time, sleep_interval=1):
164        """
165        Monitor music play on all devices, if a device's Bluetooth adapter is
166        OFF or if a device is not connected to any remote Bluetooth devices,
167        we add them to failure lists bluetooth_off_list and
168        device_not_connected_list respectively
169        :param end_time: The deadline in epoch floating point seconds that we
170            must stop playing
171        :param sleep_interval: How often to monitor, too small we may drain
172            too much resources on Android, too big the deadline might be passed
173            by a maximum of this amount
174        :return:
175            status: False iff all devices are off or disconnected otherwise True
176            bluetooth_off_list: List of ADs that have Bluetooth at OFF state
177            device_not_connected_list: List of ADs with no remote device
178                                        connected
179        """
180        device_not_connected_list = []
181        while time.time() < end_time:
182            if not self.ad.droid.bluetoothCheckState():
183                self.ad.log.error("Device {}'s Bluetooth state is off.".format(
184                    self.ad.serial))
185                return False
186            if self.ad.droid.bluetoothGetConnectedDevices() == 0:
187                self.ad.log.error(
188                    "Bluetooth device not connected. Failing test.")
189            time.sleep(sleep_interval)
190        return True
191
192    def play_music_for_duration(self, duration, sleep_interval=1):
193        """
194        A convenience method for above methods. It starts run music on all
195        devices, monitors the health of music play and stops playing them when
196        time passes the duration
197        :param duration: Duration in floating point seconds
198        :param sleep_interval: How often to check the health of music play
199        :return:
200            status: False iff all devices are off or disconnected otherwise True
201            bluetooth_off_list: List of ADs that have Bluetooth at OFF state
202            device_not_connected_list: List of ADs with no remote device
203                                        connected
204        """
205        start_time = time.time()
206        end_time = start_time + duration
207        self.start_playing_music_on_all_devices()
208        status = self.monitor_music_play_util_deadline(end_time,
209                                                       sleep_interval)
210        self.ad.droid.mediaPlayStopAll()
211        return status
212