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 17import json 18import logging 19import math 20import os 21import re 22import subprocess 23import time 24 25from acts import asserts 26from acts.controllers.ap_lib import hostapd_config 27from acts.controllers.ap_lib import hostapd_constants 28from acts.controllers.ap_lib import hostapd_security 29from acts.controllers.utils_lib.ssh import connection 30from acts.controllers.utils_lib.ssh import settings 31from acts.controllers.iperf_server import IPerfResult 32from acts.test_utils.bt import BtEnum 33from acts.test_utils.bt.bt_constants import ( 34 bluetooth_profile_connection_state_changed) 35from acts.test_utils.bt.bt_constants import bt_default_timeout 36from acts.test_utils.bt.bt_constants import bt_profile_constants 37from acts.test_utils.bt.bt_constants import bt_profile_states 38from acts.test_utils.bt.bt_constants import bt_scan_mode_types 39from acts.test_utils.bt.bt_gatt_utils import GattTestUtilsError 40from acts.test_utils.bt.bt_gatt_utils import orchestrate_gatt_connection 41from acts.test_utils.bt.bt_test_utils import disable_bluetooth 42from acts.test_utils.bt.bt_test_utils import enable_bluetooth 43from acts.test_utils.bt.bt_test_utils import is_a2dp_src_device_connected 44from acts.test_utils.bt.bt_test_utils import is_a2dp_snk_device_connected 45from acts.test_utils.bt.bt_test_utils import is_hfp_client_device_connected 46from acts.test_utils.bt.bt_test_utils import is_map_mce_device_connected 47from acts.test_utils.bt.bt_test_utils import is_map_mse_device_connected 48from acts.test_utils.bt.bt_test_utils import set_bt_scan_mode 49from acts.test_utils.car.car_telecom_utils import wait_for_active 50from acts.test_utils.car.car_telecom_utils import wait_for_dialing 51from acts.test_utils.car.car_telecom_utils import wait_for_not_in_call 52from acts.test_utils.car.car_telecom_utils import wait_for_ringing 53from acts.test_utils.tel.tel_test_utils import get_phone_number 54from acts.test_utils.tel.tel_test_utils import hangup_call 55from acts.test_utils.tel.tel_test_utils import initiate_call 56from acts.test_utils.tel.tel_test_utils import run_multithread_func 57from acts.test_utils.tel.tel_test_utils import setup_droid_properties 58from acts.test_utils.tel.tel_test_utils import wait_and_answer_call 59from acts.test_utils.wifi.wifi_power_test_utils import bokeh_plot 60from acts.test_utils.wifi.wifi_test_utils import reset_wifi 61from acts.test_utils.wifi.wifi_test_utils import wifi_connect 62from acts.test_utils.wifi.wifi_test_utils import wifi_test_device_init 63from acts.test_utils.wifi.wifi_test_utils import wifi_toggle_state 64from acts.utils import exe_cmd, create_dir 65from bokeh.layouts import column 66from bokeh.models import tools as bokeh_tools 67from bokeh.plotting import figure, output_file, save 68 69THROUGHPUT_THRESHOLD = 100 70AP_START_TIME = 10 71DISCOVERY_TIME = 10 72BLUETOOTH_WAIT_TIME = 2 73AVRCP_WAIT_TIME = 3 74 75 76def avrcp_actions(pri_ad, audio_receiver): 77 """Performs avrcp controls like volume up, volume down, skip next and 78 skip previous. 79 80 Args: 81 pri_ad: Android device. 82 audio_receiver: Relay instance. 83 84 Returns: 85 True if successful, otherwise False. 86 """ 87 if "Volume_up" and "Volume_down" in (audio_receiver.relays.keys()): 88 current_volume = pri_ad.droid.getMediaVolume() 89 audio_receiver.press_volume_up() 90 time.sleep(AVRCP_WAIT_TIME) 91 if current_volume == pri_ad.droid.getMediaVolume(): 92 pri_ad.log.error("Increase volume failed") 93 return False 94 time.sleep(AVRCP_WAIT_TIME) 95 current_volume = pri_ad.droid.getMediaVolume() 96 audio_receiver.press_volume_down() 97 time.sleep(AVRCP_WAIT_TIME) 98 if current_volume == pri_ad.droid.getMediaVolume(): 99 pri_ad.log.error("Decrease volume failed") 100 return False 101 else: 102 pri_ad.log.warning("No volume control pins specfied in relay config.") 103 104 if "Next" and "Previous" in audio_receiver.relays.keys(): 105 audio_receiver.press_next() 106 time.sleep(AVRCP_WAIT_TIME) 107 audio_receiver.press_previous() 108 time.sleep(AVRCP_WAIT_TIME) 109 else: 110 pri_ad.log.warning("No track change pins specfied in relay config.") 111 return True 112 113 114def connect_ble(pri_ad, sec_ad): 115 """Connect BLE device from DUT. 116 117 Args: 118 pri_ad: An android device object. 119 sec_ad: An android device object. 120 121 Returns: 122 True if successful, otherwise False. 123 """ 124 adv_instances = [] 125 gatt_server_list = [] 126 bluetooth_gatt_list = [] 127 pri_ad.droid.bluetoothEnableBLE() 128 sec_ad.droid.bluetoothEnableBLE() 129 gatt_server_cb = sec_ad.droid.gattServerCreateGattServerCallback() 130 gatt_server = sec_ad.droid.gattServerOpenGattServer(gatt_server_cb) 131 gatt_server_list.append(gatt_server) 132 try: 133 bluetooth_gatt, gatt_callback, adv_callback = ( 134 orchestrate_gatt_connection(pri_ad, sec_ad)) 135 bluetooth_gatt_list.append(bluetooth_gatt) 136 except GattTestUtilsError as err: 137 pri_ad.log.error(err) 138 return False 139 adv_instances.append(adv_callback) 140 connected_devices = sec_ad.droid.gattServerGetConnectedDevices(gatt_server) 141 pri_ad.log.debug("Connected device = {}".format(connected_devices)) 142 return True 143 144 145def collect_bluetooth_manager_dumpsys_logs(pri_ad, test_name): 146 """Collect "adb shell dumpsys bluetooth_manager" logs. 147 148 Args: 149 pri_ad: An android device. 150 test_name: Current test case name. 151 152 Returns: 153 Dumpsys file path. 154 """ 155 dump_counter = 0 156 dumpsys_path = os.path.join(pri_ad.log_path, test_name, "BluetoothDumpsys") 157 create_dir(dumpsys_path) 158 while os.path.exists( 159 os.path.join(dumpsys_path, 160 "bluetooth_dumpsys_%s.txt" % dump_counter)): 161 dump_counter += 1 162 out_file = "bluetooth_dumpsys_%s.txt" % dump_counter 163 cmd = "adb -s {} shell dumpsys bluetooth_manager > {}/{}".format( 164 pri_ad.serial, dumpsys_path, out_file) 165 exe_cmd(cmd) 166 file_path = os.path.join(dumpsys_path, out_file) 167 return file_path 168 169 170def configure_and_start_ap(ap, network): 171 """Configure hostapd parameters and starts access point. 172 173 Args: 174 ap: An access point object. 175 network: A dictionary with wifi network details. 176 """ 177 hostapd_sec = None 178 if network["security"] == "wpa2": 179 hostapd_sec = hostapd_security.Security( 180 security_mode=network["security"], password=network["password"]) 181 182 config = hostapd_config.HostapdConfig( 183 n_capabilities=[hostapd_constants.N_CAPABILITY_HT40_MINUS], 184 mode=hostapd_constants.MODE_11N_PURE, 185 channel=network["channel"], 186 ssid=network["SSID"], 187 security=hostapd_sec) 188 ap.start_ap(config) 189 time.sleep(AP_START_TIME) 190 191 192def connect_dev_to_headset(pri_droid, dev_to_connect, profiles_set): 193 """Connects primary android device to headset. 194 195 Args: 196 pri_droid: Android device initiating connection. 197 dev_to_connect: Third party headset mac address. 198 profiles_set: Profiles to be connected. 199 200 Returns: 201 True if Pass 202 False if Fail 203 """ 204 supported_profiles = bt_profile_constants.values() 205 for profile in profiles_set: 206 if profile not in supported_profiles: 207 pri_droid.log.info("Profile {} is not supported list {}".format( 208 profile, supported_profiles)) 209 return False 210 211 paired = False 212 for paired_device in pri_droid.droid.bluetoothGetBondedDevices(): 213 if paired_device['address'] == dev_to_connect: 214 paired = True 215 break 216 217 if not paired: 218 pri_droid.log.info("{} not paired to {}".format(pri_droid.serial, 219 dev_to_connect)) 220 return False 221 222 end_time = time.time() + 10 223 profile_connected = set() 224 sec_addr = dev_to_connect 225 pri_droid.log.info("Profiles to be connected {}".format(profiles_set)) 226 227 while (time.time() < end_time and 228 not profile_connected.issuperset(profiles_set)): 229 if (bt_profile_constants['headset_client'] not in profile_connected and 230 bt_profile_constants['headset_client'] in profiles_set): 231 if is_hfp_client_device_connected(pri_droid, sec_addr): 232 profile_connected.add(bt_profile_constants['headset_client']) 233 if (bt_profile_constants['headset'] not in profile_connected and 234 bt_profile_constants['headset'] in profiles_set): 235 profile_connected.add(bt_profile_constants['headset']) 236 if (bt_profile_constants['a2dp'] not in profile_connected and 237 bt_profile_constants['a2dp'] in profiles_set): 238 if is_a2dp_src_device_connected(pri_droid, sec_addr): 239 profile_connected.add(bt_profile_constants['a2dp']) 240 if (bt_profile_constants['a2dp_sink'] not in profile_connected and 241 bt_profile_constants['a2dp_sink'] in profiles_set): 242 if is_a2dp_snk_device_connected(pri_droid, sec_addr): 243 profile_connected.add(bt_profile_constants['a2dp_sink']) 244 if (bt_profile_constants['map_mce'] not in profile_connected and 245 bt_profile_constants['map_mce'] in profiles_set): 246 if is_map_mce_device_connected(pri_droid, sec_addr): 247 profile_connected.add(bt_profile_constants['map_mce']) 248 if (bt_profile_constants['map'] not in profile_connected and 249 bt_profile_constants['map'] in profiles_set): 250 if is_map_mse_device_connected(pri_droid, sec_addr): 251 profile_connected.add(bt_profile_constants['map']) 252 time.sleep(0.1) 253 254 while not profile_connected.issuperset(profiles_set): 255 try: 256 time.sleep(10) 257 profile_event = pri_droid.ed.pop_event( 258 bluetooth_profile_connection_state_changed, 259 bt_default_timeout + 10) 260 pri_droid.log.info("Got event {}".format(profile_event)) 261 except Exception: 262 pri_droid.log.error("Did not get {} profiles left {}".format( 263 bluetooth_profile_connection_state_changed, profile_connected)) 264 return False 265 profile = profile_event['data']['profile'] 266 state = profile_event['data']['state'] 267 device_addr = profile_event['data']['addr'] 268 if state == bt_profile_states['connected'] and ( 269 device_addr == dev_to_connect): 270 profile_connected.add(profile) 271 pri_droid.log.info( 272 "Profiles connected until now {}".format(profile_connected)) 273 return True 274 275 276def device_discoverable(pri_ad, sec_ad): 277 """Verifies whether the device is discoverable or not. 278 279 Args: 280 pri_ad: An primary android device object. 281 sec_ad: An secondary android device object. 282 283 Returns: 284 True if the device found, False otherwise. 285 """ 286 pri_ad.droid.bluetoothMakeDiscoverable() 287 scan_mode = pri_ad.droid.bluetoothGetScanMode() 288 if scan_mode == bt_scan_mode_types['connectable_discoverable']: 289 pri_ad.log.info("Primary device scan mode is " 290 "SCAN_MODE_CONNECTABLE_DISCOVERABLE.") 291 else: 292 pri_ad.log.info("Primary device scan mode is not " 293 "SCAN_MODE_CONNECTABLE_DISCOVERABLE.") 294 return False 295 if sec_ad.droid.bluetoothStartDiscovery(): 296 time.sleep(DISCOVERY_TIME) 297 droid_name = pri_ad.droid.bluetoothGetLocalName() 298 droid_address = pri_ad.droid.bluetoothGetLocalAddress() 299 get_discovered_devices = sec_ad.droid.bluetoothGetDiscoveredDevices() 300 find_flag = False 301 302 if get_discovered_devices: 303 for device in get_discovered_devices: 304 if 'name' in device and device['name'] == droid_name or ( 305 'address' in device and 306 device["address"] == droid_address): 307 pri_ad.log.info("Primary device is in the discovery " 308 "list of secondary device.") 309 find_flag = True 310 break 311 else: 312 pri_ad.log.info("Secondary device get all the discovered devices " 313 "list is empty") 314 return False 315 else: 316 pri_ad.log.info("Secondary device start discovery process error.") 317 return False 318 if not find_flag: 319 return False 320 return True 321 322 323def device_discoverability(required_devices): 324 """Wrapper function to keep required_devices in discoverable mode. 325 326 Args: 327 required_devices: List of devices to be discovered. 328 329 Returns: 330 discovered_devices: List of BD_ADDR of devices in discoverable mode. 331 """ 332 discovered_devices = [] 333 if "AndroidDevice" in required_devices: 334 discovered_devices.extend( 335 android_device_discoverability(required_devices["AndroidDevice"])) 336 if "RelayDevice" in required_devices: 337 discovered_devices.extend( 338 relay_device_discoverability(required_devices["RelayDevice"])) 339 return discovered_devices 340 341 342def android_device_discoverability(droid_dev): 343 """To keep android devices in discoverable mode. 344 345 Args: 346 droid_dev: Android device object. 347 348 Returns: 349 device_list: List of device discovered. 350 """ 351 device_list = [] 352 for device in range(len(droid_dev)): 353 inquiry_device = droid_dev[device] 354 if enable_bluetooth(inquiry_device.droid, inquiry_device.ed): 355 if set_bt_scan_mode(inquiry_device, 356 bt_scan_mode_types['connectable_discoverable']): 357 device_list.append( 358 inquiry_device.droid.bluetoothGetLocalAddress()) 359 else: 360 droid_dev.log.error( 361 "Device {} scan mode is not in" 362 "SCAN_MODE_CONNECTABLE_DISCOVERABLE.".format( 363 inquiry_device.droid.bluetoothGetLocalAddress())) 364 return device_list 365 366 367def relay_device_discoverability(relay_devices): 368 """To keep relay controlled devices in discoverable mode. 369 370 Args: 371 relay_devices: Relay object. 372 373 Returns: 374 mac_address: Mac address of relay controlled device. 375 """ 376 relay_device = relay_devices[0] 377 relay_device.power_on() 378 relay_device.enter_pairing_mode() 379 return relay_device.mac_address 380 381 382def disconnect_headset_from_dev(pri_ad, sec_ad, profiles_list): 383 """Disconnect primary from secondary on a specific set of profiles 384 385 Args: 386 pri_ad: Primary android_device initiating disconnection 387 sec_ad: Secondary android droid (sl4a interface to keep the 388 method signature the same connect_pri_to_sec above) 389 profiles_list: List of profiles we want to disconnect from 390 391 Returns: 392 True on Success 393 False on Failure 394 """ 395 supported_profiles = bt_profile_constants.values() 396 for profile in profiles_list: 397 if profile not in supported_profiles: 398 pri_ad.log.info("Profile {} is not in supported list {}".format( 399 profile, supported_profiles)) 400 return False 401 402 pri_ad.log.info(pri_ad.droid.bluetoothGetBondedDevices()) 403 404 try: 405 pri_ad.droid.bluetoothDisconnectConnectedProfile(sec_ad, profiles_list) 406 except Exception as err: 407 pri_ad.log.error( 408 "Exception while trying to disconnect profile(s) {}: {}".format( 409 profiles_list, err)) 410 return False 411 412 profile_disconnected = set() 413 pri_ad.log.info("Disconnecting from profiles: {}".format(profiles_list)) 414 415 while not profile_disconnected.issuperset(profiles_list): 416 try: 417 profile_event = pri_ad.ed.pop_event( 418 bluetooth_profile_connection_state_changed, bt_default_timeout) 419 pri_ad.log.info("Got event {}".format(profile_event)) 420 except Exception: 421 pri_ad.log.warning("Did not disconnect from Profiles") 422 return True 423 424 profile = profile_event['data']['profile'] 425 state = profile_event['data']['state'] 426 device_addr = profile_event['data']['addr'] 427 428 if state == bt_profile_states['disconnected'] and ( 429 device_addr == sec_ad): 430 profile_disconnected.add(profile) 431 pri_ad.log.info( 432 "Profiles disconnected so far {}".format(profile_disconnected)) 433 434 return True 435 436 437def initiate_disconnect_from_hf(audio_receiver, pri_ad, sec_ad, duration): 438 """Initiates call and disconnect call on primary device. 439 440 Steps: 441 1. Initiate call from HF. 442 2. Wait for dialing state at DUT and wait for ringing at secondary device. 443 3. Accepts call from secondary device. 444 4. Wait for call active state at primary and secondary device. 445 5. Sleeps until given duration. 446 6. Disconnect call from primary device. 447 7. Wait for call is not present state. 448 449 Args: 450 audio_receiver: An relay device object. 451 pri_ad: An android device to disconnect call. 452 sec_ad: An android device accepting call. 453 duration: Duration of call in seconds. 454 455 Returns: 456 True if successful, False otherwise. 457 """ 458 audio_receiver.press_initiate_call() 459 time.sleep(2) 460 flag = True 461 flag &= wait_for_dialing(logging, pri_ad) 462 flag &= wait_for_ringing(logging, sec_ad) 463 if not flag: 464 pri_ad.log.error("Outgoing call did not get established") 465 return False 466 467 if not wait_and_answer_call(logging, sec_ad): 468 pri_ad.log.error("Failed to answer call in second device.") 469 return False 470 if not wait_for_active(logging, pri_ad): 471 pri_ad.log.error("AG not in Active state.") 472 return False 473 if not wait_for_active(logging, sec_ad): 474 pri_ad.log.error("RE not in Active state.") 475 return False 476 time.sleep(duration) 477 if not hangup_call(logging, pri_ad): 478 pri_ad.log.error("Failed to hangup call.") 479 return False 480 flag = True 481 flag &= wait_for_not_in_call(logging, pri_ad) 482 flag &= wait_for_not_in_call(logging, sec_ad) 483 return flag 484 485 486def initiate_disconnect_call_dut(pri_ad, sec_ad, duration, callee_number): 487 """Initiates call and disconnect call on primary device. 488 489 Steps: 490 1. Initiate call from DUT. 491 2. Wait for dialing state at DUT and wait for ringing at secondary device. 492 3. Accepts call from secondary device. 493 4. Wait for call active state at primary and secondary device. 494 5. Sleeps until given duration. 495 6. Disconnect call from primary device. 496 7. Wait for call is not present state. 497 498 Args: 499 pri_ad: An android device to disconnect call. 500 sec_ad: An android device accepting call. 501 duration: Duration of call in seconds. 502 callee_number: Secondary device's phone number. 503 504 Returns: 505 True if successful, False otherwise. 506 """ 507 if not initiate_call(logging, pri_ad, callee_number): 508 pri_ad.log.error("Failed to initiate call") 509 return False 510 time.sleep(2) 511 512 flag = True 513 flag &= wait_for_dialing(logging, pri_ad) 514 flag &= wait_for_ringing(logging, sec_ad) 515 if not flag: 516 pri_ad.log.error("Outgoing call did not get established") 517 return False 518 519 if not wait_and_answer_call(logging, sec_ad): 520 pri_ad.log.error("Failed to answer call in second device.") 521 return False 522 # Wait for AG, RE to go into an Active state. 523 if not wait_for_active(logging, pri_ad): 524 pri_ad.log.error("AG not in Active state.") 525 return False 526 if not wait_for_active(logging, sec_ad): 527 pri_ad.log.error("RE not in Active state.") 528 return False 529 time.sleep(duration) 530 if not hangup_call(logging, pri_ad): 531 pri_ad.log.error("Failed to hangup call.") 532 return False 533 flag = True 534 flag &= wait_for_not_in_call(logging, pri_ad) 535 flag &= wait_for_not_in_call(logging, sec_ad) 536 537 return flag 538 539 540def check_wifi_status(pri_ad, network, ssh_config): 541 """Function to check existence of wifi connection. 542 543 Args: 544 pri_ad: An android device. 545 network: network ssid. 546 ssh_config: ssh config for iperf client. 547 """ 548 time.sleep(5) 549 proc = subprocess.Popen("pgrep -f 'iperf3 -c'", stdout=subprocess.PIPE, shell=True) 550 pid_list = proc.communicate()[0].decode('utf-8').split() 551 552 while True: 553 p = subprocess.Popen(["pgrep", "-f", "iperf3"], stdout=subprocess.PIPE) 554 process_list = p.communicate()[0].decode('utf-8').split() 555 if not wifi_connection_check(pri_ad, network["SSID"]): 556 pri_ad.adb.shell("killall iperf3") 557 if ssh_config: 558 time.sleep(5) 559 ssh_settings = settings.from_config(ssh_config) 560 ssh_session = connection.SshConnection(ssh_settings) 561 result = ssh_session.run("pgrep iperf3") 562 res = result.stdout.split("\n") 563 for pid in res: 564 try: 565 ssh_session.run("kill -9 %s" %pid) 566 except Exception as e: 567 logging.warning("No such process: %s" %e) 568 for pid in pid_list[:-1]: 569 subprocess.Popen("kill -9 {}".format(pid), 570 stdout=subprocess.PIPE, shell=True) 571 break 572 elif pid_list[0] not in process_list: 573 break 574 575 576def iperf_result(log, protocol, result): 577 """Accepts the iperf result in json format and parse the output to 578 get throughput value. 579 580 Args: 581 log: Logger object. 582 protocol : TCP or UDP protocol. 583 result: iperf result's filepath. 584 585 Returns: 586 rx_rate: Data received from client. 587 """ 588 if os.path.exists(result): 589 ip_cl = IPerfResult(result) 590 591 if protocol == "udp": 592 rx_rate = (math.fsum(ip_cl.instantaneous_rates) / 593 len(ip_cl.instantaneous_rates))*8 594 else: 595 rx_rate = ip_cl.avg_receive_rate * 8 596 return rx_rate 597 else: 598 log.error("IPerf file not found") 599 return False 600 601 602def is_a2dp_connected(pri_ad, headset_mac_address): 603 """Convenience Function to see if the 2 devices are connected on A2DP. 604 605 Args: 606 pri_ad : An android device. 607 headset_mac_address : Mac address of headset. 608 609 Returns: 610 True:If A2DP connection exists, False otherwise. 611 """ 612 devices = pri_ad.droid.bluetoothA2dpGetConnectedDevices() 613 for device in devices: 614 pri_ad.log.debug("A2dp Connected device {}".format(device["name"])) 615 if device["address"] == headset_mac_address: 616 return True 617 return False 618 619 620def media_stream_check(pri_ad, duration, headset_mac_address): 621 """Checks whether A2DP connecion is active or not for given duration of 622 time. 623 624 Args: 625 pri_ad : An android device. 626 duration : No of seconds to check if a2dp streaming is alive. 627 headset_mac_address : Headset mac address. 628 629 Returns: 630 True: If A2dp connection is active for entire duration. 631 False: If A2dp connection is not active. 632 """ 633 while time.time() < duration: 634 if not is_a2dp_connected(pri_ad, headset_mac_address): 635 pri_ad.log.error('A2dp connection not active at %s', pri_ad.serial) 636 return False 637 time.sleep(1) 638 return True 639 640 641def multithread_func(log, tasks): 642 """Multi-thread function wrapper. 643 644 Args: 645 log: log object. 646 tasks: tasks to be executed in parallel. 647 648 Returns: 649 List of results of tasks 650 """ 651 results = run_multithread_func(log, tasks) 652 for res in results: 653 if not res: 654 return False 655 return True 656 657 658def music_play_and_check(pri_ad, headset_mac_address, music_to_play, duration): 659 """Starts playing media and checks if media plays for n seconds. 660 661 Steps: 662 1. Starts media player on android device. 663 2. Checks if music streaming is ongoing for n seconds. 664 3. Stops media player. 665 4. Collect dumpsys logs. 666 667 Args: 668 pri_ad: An android device. 669 headset_mac_address: Mac address of third party headset. 670 music_to_play: Indicates the music file to play. 671 duration: Time in secs to indicate music time to play. 672 673 Returns: 674 True if successful, False otherwise. 675 """ 676 pri_ad.droid.setMediaVolume(pri_ad.droid.getMaxMediaVolume() - 1) 677 pri_ad.log.info("current volume = {}".format(pri_ad.droid.getMediaVolume())) 678 pri_ad.log.debug("In music play and check") 679 if not start_media_play(pri_ad, music_to_play): 680 pri_ad.log.error("Start media play failed.") 681 return False 682 stream_time = time.time() + duration 683 if not media_stream_check(pri_ad, stream_time, headset_mac_address): 684 pri_ad.log.error("A2DP Connection check failed.") 685 pri_ad.droid.mediaPlayStop() 686 return False 687 pri_ad.droid.mediaPlayStop() 688 return True 689 690 691def music_play_and_check_via_app(pri_ad, headset_mac_address, duration=5): 692 """Starts google music player and check for A2DP connection. 693 694 Steps: 695 1. Starts Google music player on android device. 696 2. Checks for A2DP connection. 697 698 Args: 699 pri_ad: An android device. 700 headset_mac_address: Mac address of third party headset. 701 duration: Total time of music streaming. 702 703 Returns: 704 True if successful, False otherwise. 705 """ 706 pri_ad.adb.shell("am start com.google.android.music") 707 time.sleep(3) 708 pri_ad.adb.shell("input keyevent 85") 709 stream_time = time.time() + duration 710 try: 711 if not media_stream_check(pri_ad, stream_time, headset_mac_address): 712 pri_ad.log.error("A2dp connection not active at %s", pri_ad.serial) 713 return False 714 finally: 715 pri_ad.adb.shell("am force-stop com.google.android.music") 716 return True 717 718 719def get_phone_ip(ad): 720 """Get the WiFi IP address of the phone. 721 722 Args: 723 ad: the android device under test 724 725 Returns: 726 Ip address of the phone for WiFi, as a string 727 """ 728 return ad.droid.connectivityGetIPv4Addresses('wlan0')[0] 729 730 731def pair_dev_to_headset(pri_ad, dev_to_pair): 732 """Pairs pri droid to secondary droid. 733 734 Args: 735 pri_ad: Android device initiating connection 736 dev_to_pair: Third party headset mac address. 737 738 Returns: 739 True if Pass 740 False if Fail 741 """ 742 bonded_devices = pri_ad.droid.bluetoothGetBondedDevices() 743 for d in bonded_devices: 744 if d['address'] == dev_to_pair: 745 pri_ad.log.info("Successfully bonded to device".format(dev_to_pair)) 746 return True 747 pri_ad.droid.bluetoothStartDiscovery() 748 time.sleep(10) #Wait until device gets discovered 749 pri_ad.droid.bluetoothCancelDiscovery() 750 pri_ad.log.debug("discovered devices = {}".format( 751 pri_ad.droid.bluetoothGetDiscoveredDevices())) 752 for device in pri_ad.droid.bluetoothGetDiscoveredDevices(): 753 if device['address'] == dev_to_pair: 754 755 result = pri_ad.droid.bluetoothDiscoverAndBond(dev_to_pair) 756 pri_ad.log.info(result) 757 end_time = time.time() + bt_default_timeout 758 pri_ad.log.info("Verifying devices are bonded") 759 time.sleep(5) #Wait time until device gets paired. 760 while time.time() < end_time: 761 bonded_devices = pri_ad.droid.bluetoothGetBondedDevices() 762 bonded = False 763 for d in bonded_devices: 764 if d['address'] == dev_to_pair: 765 pri_ad.log.info( 766 "Successfully bonded to device".format(dev_to_pair)) 767 return True 768 pri_ad.log.info("Failed to bond devices.") 769 return False 770 771 772def pair_and_connect_headset(pri_ad, headset_mac_address, profile_to_connect, retry=5): 773 """Pair and connect android device with third party headset. 774 775 Args: 776 pri_ad: An android device. 777 headset_mac_address: Mac address of third party headset. 778 profile_to_connect: Profile to be connected with headset. 779 retry: Number of times pair and connection should happen. 780 781 Returns: 782 True if pair and connect to headset successful, False otherwise. 783 """ 784 785 paired = False 786 for _ in range(retry): 787 if pair_dev_to_headset(pri_ad, headset_mac_address): 788 paired = True 789 break 790 else: 791 pri_ad.log.error("Could not pair to headset. Retrying.") 792 793 time.sleep(2) # Wait until pairing gets over. 794 795 if paired: 796 for _ in range(retry): 797 if connect_dev_to_headset(pri_ad, headset_mac_address, 798 profile_to_connect): 799 return True 800 else: 801 pri_ad.log.error("Could not connect to headset. Retrying.") 802 else: 803 asserts.fail("Failed to pair and connect to headset") 804 805 806def perform_classic_discovery(pri_ad, duration, file_name, dev_list=None): 807 """Convenience function to start and stop device discovery. 808 809 Args: 810 pri_ad: An android device. 811 duration: iperf duration of the test. 812 file_name: Json file to which result is dumped 813 dev_list: List of devices to be discoverable mode. 814 815 Returns: 816 True start and stop discovery is successful, False otherwise. 817 """ 818 if dev_list: 819 devices_required = device_discoverability(dev_list) 820 else: 821 devices_required = None 822 iteration = 0 823 result = {} 824 result["discovered_devices"] = {} 825 discover_result = [] 826 start_time = time.time() 827 while time.time() < start_time + duration: 828 if not pri_ad.droid.bluetoothStartDiscovery(): 829 pri_ad.log.error("Failed to start discovery") 830 return False 831 time.sleep(DISCOVERY_TIME) 832 if not pri_ad.droid.bluetoothCancelDiscovery(): 833 pri_ad.log.error("Failed to cancel discovery") 834 return False 835 pri_ad.log.info("Discovered device list {}".format( 836 pri_ad.droid.bluetoothGetDiscoveredDevices())) 837 if devices_required is not None: 838 result["discovered_devices"][iteration] = [] 839 devices_name = { 840 element.get('name', element['address']) 841 for element in pri_ad.droid.bluetoothGetDiscoveredDevices() 842 if element["address"] in devices_required 843 } 844 result["discovered_devices"][iteration] = list(devices_name) 845 discover_result.extend([len(devices_name) == len(devices_required)]) 846 iteration += 1 847 with open(file_name, 'a') as results_file: 848 json.dump(result, results_file, indent=4) 849 if False in discover_result: 850 return False 851 else: 852 pri_ad.log.warning("No devices are kept in discoverable mode") 853 return True 854 855 856def connect_wlan_profile(pri_ad, network): 857 """Disconnect and Connect to AP. 858 859 Args: 860 pri_ad: An android device. 861 network: Network to which AP to be connected. 862 863 Returns: 864 True if successful, False otherwise. 865 """ 866 reset_wifi(pri_ad) 867 wifi_toggle_state(pri_ad, False) 868 wifi_test_device_init(pri_ad) 869 wifi_connect(pri_ad, network) 870 if not wifi_connection_check(pri_ad, network["SSID"]): 871 pri_ad.log.error("Wifi connection does not exist.") 872 return False 873 return True 874 875 876def toggle_bluetooth(pri_ad, duration): 877 """Toggles bluetooth on/off for N iterations. 878 879 Args: 880 pri_ad: An android device object. 881 duration: Iperf duration of the test. 882 883 Returns: 884 True if successful, False otherwise. 885 """ 886 start_time = time.time() 887 while time.time() < start_time + duration: 888 if not enable_bluetooth(pri_ad.droid, pri_ad.ed): 889 pri_ad.log.error("Failed to enable bluetooth") 890 return False 891 time.sleep(BLUETOOTH_WAIT_TIME) 892 if not disable_bluetooth(pri_ad.droid): 893 pri_ad.log.error("Failed to turn off bluetooth") 894 return False 895 time.sleep(BLUETOOTH_WAIT_TIME) 896 return True 897 898 899def toggle_screen_state(pri_ad, duration): 900 """Toggles the screen state to on or off.. 901 902 Args: 903 pri_ad: Android device. 904 duration: Iperf duration of the test. 905 906 Returns: 907 True if successful, False otherwise. 908 """ 909 start_time = time.time() 910 while time.time() < start_time + duration: 911 if not pri_ad.ensure_screen_on(): 912 pri_ad.log.error("User window cannot come up") 913 return False 914 if not pri_ad.go_to_sleep(): 915 pri_ad.log.info("Screen off") 916 return True 917 918 919def setup_tel_config(pri_ad, sec_ad, sim_conf_file): 920 """Sets tel properties for primary device and secondary devices 921 922 Args: 923 pri_ad: An android device object. 924 sec_ad: An android device object. 925 sim_conf_file: Sim card map. 926 927 Returns: 928 pri_ad_num: Phone number of primary device. 929 sec_ad_num: Phone number of secondary device. 930 """ 931 setup_droid_properties(logging, pri_ad, sim_conf_file) 932 pri_ad_num = get_phone_number(logging, pri_ad) 933 setup_droid_properties(logging, sec_ad, sim_conf_file) 934 sec_ad_num = get_phone_number(logging, sec_ad) 935 return pri_ad_num, sec_ad_num 936 937 938def start_fping(pri_ad, duration, fping_params): 939 """Starts fping to ping for DUT's ip address. 940 941 Steps: 942 1. Run fping command to check DUT's IP is alive or not. 943 944 Args: 945 pri_ad: An android device object. 946 duration: Duration of fping in seconds. 947 fping_params: List of parameters for fping to run. 948 949 Returns: 950 True if successful, False otherwise. 951 """ 952 counter = 0 953 fping_path = ''.join((pri_ad.log_path, "/Fping")) 954 create_dir(fping_path) 955 while os.path.isfile(fping_path + "/fping_%s.txt" % counter): 956 counter += 1 957 out_file_name = "{}".format("fping_%s.txt" % counter) 958 959 full_out_path = os.path.join(fping_path, out_file_name) 960 cmd = "fping {} -D -c {}".format(get_phone_ip(pri_ad), duration) 961 if fping_params["ssh_config"]: 962 ssh_settings = settings.from_config(fping_params["ssh_config"]) 963 ssh_session = connection.SshConnection(ssh_settings) 964 try: 965 with open(full_out_path, 'w') as outfile: 966 job_result = ssh_session.run(cmd) 967 outfile.write(job_result.stdout) 968 outfile.write("\n") 969 outfile.writelines(job_result.stderr) 970 except Exception as err: 971 pri_ad.log.error("Fping run has been failed. = {}".format(err)) 972 return False 973 else: 974 cmd = cmd.split() 975 with open(full_out_path, "w") as f: 976 subprocess.call(cmd, stderr=f, stdout=f) 977 result = parse_fping_results(fping_params["fping_drop_tolerance"], 978 full_out_path) 979 return bool(result) 980 981 982def parse_fping_results(failure_rate, full_out_path): 983 """Calculates fping results. 984 985 Steps: 986 1. Read the file and calculate the results. 987 988 Args: 989 failure_rate: Fping packet drop tolerance value. 990 full_out_path: path where the fping results has been stored. 991 992 Returns: 993 loss_percent: loss percentage of fping packet. 994 """ 995 try: 996 result_file = open(full_out_path, "r") 997 lines = result_file.readlines() 998 res_line = lines[-1] 999 # Ex: res_line = "192.168.186.224 : xmt/rcv/%loss = 10/10/0%, 1000 # min/avg/max = 36.7/251/1272" 1001 loss_percent = re.search("[0-9]+%", res_line) 1002 if int(loss_percent.group().strip("%")) > failure_rate: 1003 logging.error("Packet drop observed") 1004 return False 1005 return loss_percent.group() 1006 except Exception as e: 1007 logging.error("Error in parsing fping results : %s" %(e)) 1008 return False 1009 1010 1011def start_media_play(pri_ad, music_file_to_play): 1012 """Starts media player on device. 1013 1014 Args: 1015 pri_ad : An android device. 1016 music_file_to_play : An audio file to play. 1017 1018 Returns: 1019 True:If media player start music, False otherwise. 1020 """ 1021 if not pri_ad.droid.mediaPlayOpen( 1022 "file:///sdcard/Music/{}".format(music_file_to_play)): 1023 pri_ad.log.error("Failed to play music") 1024 return False 1025 1026 pri_ad.droid.mediaPlaySetLooping(True) 1027 pri_ad.log.info("Music is now playing on device {}".format(pri_ad.serial)) 1028 return True 1029 1030 1031def wifi_connection_check(pri_ad, ssid): 1032 """Function to check existence of wifi connection. 1033 1034 Args: 1035 pri_ad : An android device. 1036 ssid : wifi ssid to check. 1037 1038 Returns: 1039 True if wifi connection exists, False otherwise. 1040 """ 1041 wifi_info = pri_ad.droid.wifiGetConnectionInfo() 1042 if (wifi_info["SSID"] == ssid and 1043 wifi_info["supplicant_state"] == "completed"): 1044 return True 1045 pri_ad.log.error("Wifi Connection check failed : {}".format(wifi_info)) 1046 return False 1047 1048 1049def push_music_to_android_device(ad, audio_params): 1050 """Add music to Android device as specified by the test config 1051 1052 Args: 1053 ad: Android device 1054 audio_params: Music file to push. 1055 1056 Returns: 1057 True on success, False on failure 1058 """ 1059 ad.log.info("Pushing music to the Android device") 1060 android_music_path = "/sdcard/Music/" 1061 music_path = audio_params["music_file"] 1062 if type(music_path) is list: 1063 ad.log.info("Media ready to push as is.") 1064 for item in music_path: 1065 music_file_to_play = item 1066 ad.adb.push(item, android_music_path) 1067 return music_file_to_play 1068 else: 1069 music_file_to_play = audio_params["music_file"] 1070 ad.adb.push("{} {}".format(music_file_to_play, android_music_path)) 1071 return (os.path.basename(music_file_to_play)) 1072 1073 1074def bokeh_chart_plot(bt_attenuation_range, 1075 data_sets, 1076 legends, 1077 fig_property, 1078 shaded_region=None, 1079 output_file_path=None): 1080 """Plot bokeh figs. 1081 1082 Args: 1083 bt_attenuation_range: range of BT attenuation. 1084 data_sets: data sets including lists of x_data and lists of y_data 1085 ex: [[[x_data1], [x_data2]], [[y_data1],[y_data2]]] 1086 legends: list of legend for each curve 1087 fig_property: dict containing the plot property, including title, 1088 lables, linewidth, circle size, etc. 1089 shaded_region: optional dict containing data for plot shading 1090 output_file_path: optional path at which to save figure 1091 1092 Returns: 1093 plot: bokeh plot figure object 1094 """ 1095 TOOLS = ('box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save') 1096 colors = [ 1097 'red', 'green', 'blue', 'olive', 'orange', 'salmon', 'black', 'navy', 1098 'yellow', 'darkred', 'goldenrod' 1099 ] 1100 plot = [] 1101 data = [[], []] 1102 legend = [] 1103 for i in bt_attenuation_range: 1104 if "Packet drop" in legends[i][0]: 1105 plot_info = {0: "A2dp_packet_drop_plot", 1: "throughput_plot"} 1106 else: 1107 plot_info = {0: "throughput_plot"} 1108 for j in plot_info: 1109 if "Packet drops" in legends[i][j]: 1110 if data_sets[i]["a2dp_packet_drops"]: 1111 plot_i_j = figure( 1112 plot_width=1000, 1113 plot_height=500, 1114 title=fig_property['title'], 1115 tools=TOOLS) 1116 1117 plot_i_j.add_tools( 1118 bokeh_tools.WheelZoomTool(dimensions="width")) 1119 plot_i_j.add_tools( 1120 bokeh_tools.WheelZoomTool(dimensions="height")) 1121 plot_i_j.xaxis.axis_label = fig_property['x_label'] 1122 plot_i_j.yaxis.axis_label = fig_property['y_label'][j] 1123 plot_i_j.legend.location = "top_right" 1124 plot_i_j.legend.click_policy = "hide" 1125 plot_i_j.title.text_font_size = {'value': '15pt'} 1126 1127 plot_i_j.line( 1128 data_sets[i]["a2dp_attenuation"], 1129 data_sets[i]["a2dp_packet_drops"], 1130 legend=legends[i][j], 1131 line_width=3, 1132 color=colors[j]) 1133 plot_i_j.circle( 1134 data_sets[i]["a2dp_attenuation"], 1135 data_sets[i]["a2dp_packet_drops"], 1136 legend=str(legends[i][j]), 1137 fill_color=colors[j]) 1138 plot.append(plot_i_j) 1139 elif "Performance Results" in legends[i][j]: 1140 plot_i_j = figure( 1141 plot_width=1000, 1142 plot_height=500, 1143 title=fig_property['title'], 1144 tools=TOOLS) 1145 plot_i_j.add_tools( 1146 bokeh_tools.WheelZoomTool(dimensions="width")) 1147 plot_i_j.add_tools( 1148 bokeh_tools.WheelZoomTool(dimensions="height")) 1149 plot_i_j.xaxis.axis_label = fig_property['x_label'] 1150 plot_i_j.yaxis.axis_label = fig_property['y_label'][j] 1151 plot_i_j.legend.location = "top_right" 1152 plot_i_j.legend.click_policy = "hide" 1153 plot_i_j.title.text_font_size = {'value': '15pt'} 1154 data[0].insert(0, data_sets[i]["attenuation"]) 1155 data[1].insert(0, data_sets[i]["throughput_received"]) 1156 legend.insert(0, legends[i][j + 1]) 1157 plot_i_j.line( 1158 data_sets[i]["user_attenuation"], 1159 data_sets[i]["user_throughput"], 1160 legend=legends[i][j], 1161 line_width=3, 1162 color=colors[j]) 1163 plot_i_j.circle( 1164 data_sets[i]["user_attenuation"], 1165 data_sets[i]["user_throughput"], 1166 legend=str(legends[i][j]), 1167 fill_color=colors[j]) 1168 plot_i_j.line( 1169 data_sets[i]["attenuation"], 1170 data_sets[i]["throughput_received"], 1171 legend=legends[i][j + 1], 1172 line_width=3, 1173 color=colors[j]) 1174 plot_i_j.circle( 1175 data_sets[i]["attenuation"], 1176 data_sets[i]["throughput_received"], 1177 legend=str(legends[i][j + 1]), 1178 fill_color=colors[j]) 1179 if shaded_region: 1180 band_x = shaded_region[i]["x_vector"] 1181 band_x.extend(shaded_region[i]["x_vector"][::-1]) 1182 band_y = shaded_region[i]["lower_limit"] 1183 band_y.extend(shaded_region[i]["upper_limit"][::-1]) 1184 plot_i_j.patch( 1185 band_x, 1186 band_y, 1187 color='#7570B3', 1188 line_alpha=0.1, 1189 fill_alpha=0.1) 1190 plot.append(plot_i_j) 1191 else: 1192 plot_i_j = figure( 1193 plot_width=1000, 1194 plot_height=500, 1195 title=fig_property['title'], 1196 tools=TOOLS) 1197 plot_i_j.add_tools( 1198 bokeh_tools.WheelZoomTool(dimensions="width")) 1199 plot_i_j.add_tools( 1200 bokeh_tools.WheelZoomTool(dimensions="height")) 1201 plot_i_j.xaxis.axis_label = fig_property['x_label'] 1202 plot_i_j.yaxis.axis_label = fig_property['y_label'][j] 1203 plot_i_j.legend.location = "top_right" 1204 plot_i_j.legend.click_policy = "hide" 1205 plot_i_j.title.text_font_size = {'value': '15pt'} 1206 data[0].insert(0, data_sets[i]["attenuation"]) 1207 data[1].insert(0, data_sets[i]["throughput_received"]) 1208 legend.insert(0, legends[i][j]) 1209 plot_i_j.line( 1210 data_sets[i]["attenuation"], 1211 data_sets[i]["throughput_received"], 1212 legend=legends[i][j], 1213 line_width=3, 1214 color=colors[j]) 1215 plot_i_j.circle( 1216 data_sets[i]["attenuation"], 1217 data_sets[i]["throughput_received"], 1218 legend=str(legends[i][j]), 1219 fill_color=colors[j]) 1220 plot.append(plot_i_j) 1221 fig_property['y_label'] = "Throughput (Mbps)" 1222 all_plot = bokeh_plot(data, legend, fig_property, shaded_region=None, 1223 output_file_path=None) 1224 plot.insert(0, all_plot) 1225 if output_file_path is not None: 1226 output_file(output_file_path) 1227 save(column(plot)) 1228 return plot 1229 1230 1231class A2dpDumpsysParser(): 1232 1233 def __init__(self): 1234 self.count_list = [] 1235 self.frame_list = [] 1236 self.dropped_count = None 1237 1238 def parse(self, file_path): 1239 """Convenience function to parse a2dp dumpsys logs. 1240 1241 Args: 1242 file_path: Path of dumpsys logs. 1243 1244 Returns: 1245 dropped_list containing packet drop count for every iteration. 1246 drop containing list of all packets dropped for test suite. 1247 """ 1248 a2dp_dumpsys_info = [] 1249 with open(file_path) as dumpsys_file: 1250 for line in dumpsys_file: 1251 if "A2DP State:" in line: 1252 a2dp_dumpsys_info.append(line) 1253 elif "Counts (max dropped)" not in line and len( 1254 a2dp_dumpsys_info) > 0: 1255 a2dp_dumpsys_info.append(line) 1256 elif "Counts (max dropped)" in line: 1257 a2dp_dumpsys_info = ''.join(a2dp_dumpsys_info) 1258 a2dp_info = a2dp_dumpsys_info.split("\n") 1259 # Ex: Frames per packet (total/max/ave) : 5034 / 1 / 0 1260 frames = int(re.split("[':/()]", str(a2dp_info[-3]))[-3]) 1261 self.frame_list.append(frames) 1262 # Ex : Counts (flushed/dropped/dropouts) : 0 / 4 / 0 1263 count = int(re.split("[':/()]", str(a2dp_info[-2]))[-2]) 1264 if count > 0: 1265 for i in range(len(self.count_list)): 1266 count = count - self.count_list[i] 1267 self.count_list.append(count) 1268 if len(self.frame_list) > 1: 1269 last_frame = self.frame_list[-1] - self.frame_list[ 1270 -2] 1271 self.dropped_count = (count / last_frame) * 100 1272 else: 1273 self.dropped_count = ( 1274 count / self.frame_list[-1]) * 100 1275 else: 1276 self.dropped_count = count 1277 logging.info(a2dp_dumpsys_info) 1278 return self.dropped_count 1279