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