1#/usr/bin/env python3
2#
3# Copyright (C) 2018 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"""Bluetooth disconnect and reconnect verification."""
17# Quick way to get the Apollo serial number:
18# python3.5 -c "from acts.controllers.buds_lib.apollo_lib import get_devices; [print(d['serial_number']) for d in get_devices()]"
19
20import statistics
21import time
22from acts import asserts
23from acts.base_test import BaseTestClass
24from acts.controllers.buds_lib.test_actions.apollo_acts import ApolloTestActions
25from acts.signals import TestFailure
26from acts.signals import TestPass
27from acts.test_decorators import test_tracker_info
28from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
29from acts.test_utils.bt.bt_test_utils import enable_bluetooth
30from acts.test_utils.bt.bt_test_utils import setup_multiple_devices_for_bt_test
31from acts.test_utils.bt.loggers.bluetooth_metric_logger import BluetoothMetricLogger
32from acts.utils import set_location_service
33
34# The number of reconnections to be attempted during the test
35RECONNECTION_ATTEMPTS = 200
36
37
38class BluetoothReconnectError(TestFailure):
39    pass
40
41
42class BluetoothReconnectTest(BaseTestClass):
43    """Connects a phone to Apollo earbuds to test Bluetooth reconnection.
44
45   Attributes:
46       phone: An Android phone object
47       apollo: An Apollo earbuds object
48       apollo_act: An Apollo test action object
49       dut_bt_addr: The Bluetooth address of the Apollo earbuds
50    """
51
52    def setup_class(self):
53        super().setup_class()
54        # sanity check of the dut devices.
55        # TODO(b/119051823): Investigate using a config validator to replace this.
56        if not self.android_devices:
57            raise ValueError(
58                'Cannot find android phone (need at least one).')
59        self.phone = self.android_devices[0]
60
61        if not self.buds_devices:
62            raise ValueError(
63                'Cannot find apollo device (need at least one).')
64        self.apollo = self.buds_devices[0]
65        self.log.info('Successfully found needed devices.')
66
67        # Staging the test, create result object, etc.
68        self.apollo_act = ApolloTestActions(self.apollo, self.log)
69        self.dut_bt_addr = self.apollo.bluetooth_address
70        self.bt_logger = BluetoothMetricLogger.for_test_case()
71
72    def setup_test(self):
73        setup_multiple_devices_for_bt_test(self.android_devices)
74        # Make sure Bluetooth is on
75        enable_bluetooth(self.phone.droid, self.phone.ed)
76        set_location_service(self.phone, True)
77        self.apollo_act.factory_reset()
78
79        # Initial pairing and connection of devices
80        self.phone.droid.bluetoothDiscoverAndBond(self.dut_bt_addr)
81        paired_and_connected = self.apollo_act.wait_for_bluetooth_a2dp_hfp()
82        asserts.assert_true(paired_and_connected,
83                            'Failed to pair and connect devices')
84        time.sleep(20)
85        self.log.info('===== START BLUETOOTH RECONNECT TEST  =====')
86
87    def teardown_test(self):
88        self.log.info('Teardown test, shutting down all services...')
89        self.apollo_act.factory_reset()
90        self.apollo.close()
91
92    def _reconnect_bluetooth_from_phone(self):
93        """Reconnects Bluetooth from the phone.
94
95        Disables and then re-enables Bluetooth from the phone when Bluetooth
96        disconnection has been verified. Measures the reconnection time.
97
98        Returns:
99            The time it takes to connect Bluetooth in milliseconds.
100
101        Raises:
102            BluetoothReconnectError
103        """
104
105        # Disconnect Bluetooth from the phone side
106        self.log.info('Disconnecting Bluetooth from phone')
107        self.phone.droid.bluetoothDisconnectConnected(self.dut_bt_addr)
108        if not self.apollo_act.wait_for_bluetooth_disconnection():
109            raise BluetoothReconnectError('Failed to disconnect Bluetooth')
110        self.log.info('Bluetooth disconnected successfully')
111
112        # Buffer between disconnect and reconnect
113        time.sleep(3)
114
115        # Reconnect Bluetooth from the phone side
116        self.log.info('Connecting Bluetooth from phone')
117        start_time = time.perf_counter()
118        self.phone.droid.bluetoothConnectBonded(self.dut_bt_addr)
119        self.log.info('Bluetooth connected successfully')
120        if not self.apollo_act.wait_for_bluetooth_a2dp_hfp():
121            raise BluetoothReconnectError('Failed to connect Bluetooth')
122        end_time = time.perf_counter()
123        return (end_time - start_time) * 1000
124
125    @BluetoothBaseTest.bt_test_wrap
126    @test_tracker_info(uuid='da921903-92d0-471d-ae01-456058cc1297')
127    def test_bluetooth_reconnect(self):
128        """Reconnects Bluetooth between a phone and Apollo device a specified
129        number of times and reports connection time statistics."""
130
131        # Store metrics
132        metrics = {}
133        connection_success = 0
134        connection_times = []
135        reconnection_failures = []
136        first_connection_failure = None
137
138        for attempt in range(RECONNECTION_ATTEMPTS):
139            self.log.info("Reconnection attempt {}".format(attempt + 1))
140            reconnect_timestamp = time.strftime('%Y-%m-%d %H:%M:%S',
141                                                time.localtime())
142            try:
143                connection_time = self._reconnect_bluetooth_from_phone()
144            except BluetoothReconnectError as err:
145                self.log.error(err)
146                failure_data = {'timestamp': reconnect_timestamp,
147                                'error': str(err),
148                                'reconnect_attempt': attempt + 1}
149                reconnection_failures.append(failure_data)
150                if not first_connection_failure:
151                    first_connection_failure = err
152            else:
153                connection_times.append(connection_time)
154                connection_success += 1
155
156            # Buffer between reconnection attempts
157            time.sleep(3)
158
159        metrics['connection_attempt_count'] = RECONNECTION_ATTEMPTS
160        metrics['connection_successful_count'] = connection_success
161        metrics['connection_failed_count'] = (RECONNECTION_ATTEMPTS
162                                              - connection_success)
163        if len(connection_times) > 0:
164            metrics['connection_max_time_millis'] = int(max(connection_times))
165            metrics['connection_min_time_millis'] = int(min(connection_times))
166            metrics['connection_avg_time_millis'] = int(statistics.mean(
167                connection_times))
168
169        if reconnection_failures:
170            metrics['connection_failure_info'] = reconnection_failures
171
172        proto = self.bt_logger.get_results(metrics,
173                                           self.__class__.__name__,
174                                           self.phone,
175                                           self.apollo)
176
177        self.log.info('Metrics: {}'.format(metrics))
178
179        if RECONNECTION_ATTEMPTS != connection_success:
180            raise TestFailure(str(first_connection_failure), extras=proto)
181        else:
182            raise TestPass('Bluetooth reconnect test passed', extras=proto)
183