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