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