1#!/usr/bin/env python3
2#
3#   Copyright 2017 Google, Inc.
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of 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,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17import logging
18import time
19from acts import utils
20from acts.libs.proc import job
21from acts.controllers.ap_lib import bridge_interface as bi
22from acts_contrib.test_utils.wifi import wifi_test_utils as wutils
23from acts.controllers.adb_lib.error import AdbCommandError
24from acts.controllers.ap_lib import hostapd_security
25from acts.controllers.ap_lib import hostapd_ap_preset
26
27# http://www.secdev.org/projects/scapy/
28# On ubuntu, sudo pip3 install scapy
29import scapy.all as scapy
30
31GET_FROM_PHONE = 'get_from_dut'
32GET_FROM_AP = 'get_from_ap'
33ENABLED_MODULATED_DTIM = 'gEnableModulatedDTIM='
34MAX_MODULATED_DTIM = 'gMaxLIModulatedDTIM='
35
36CHRE_WIFI_SCAN_TYPE = {
37    'active': 'active',
38    'passive': 'passive',
39    'activePassiveDfs': 'active_passive_dfs',
40    'noPreference': 'no_preference'
41}
42
43CHRE_WIFI_RADIO_CHAIN = {
44    'lowLatency': 'low_latency',
45    'lowPower': 'low_power',
46    'highAccuracy': 'high_accuracy'
47}
48
49CHRE_WIFI_CHANNEL_SET = {
50    'all': 'all',
51    'nonDfs': 'non_dfs'
52}
53
54
55def change_dtim(ad, gEnableModulatedDTIM, gMaxLIModulatedDTIM=10):
56    """Function to change the DTIM setting in the phone.
57
58    Args:
59        ad: the target android device, AndroidDevice object
60        gEnableModulatedDTIM: Modulated DTIM, int
61        gMaxLIModulatedDTIM: Maximum modulated DTIM, int
62    """
63    ad.log.info('Sets dtim to {}'.format(gEnableModulatedDTIM))
64
65    # In P21 the dtim setting method changed and an AdbCommandError will take
66    # place to get ini_file_phone. Thus add try/except block for the old method.
67    # If error occurs, use change_dtim_adb method later. Otherwise, first trying
68    # to find the ini file with DTIM settings
69    try:
70        ini_file_phone = ad.adb.shell('ls /vendor/firmware/wlan/*/*.ini')
71
72    except AdbCommandError as e:
73
74        # Gets AdbCommandError, change dtim later with change_dtim_adb merthod.
75        # change_dtim_adb requires that wifi connection is on.
76        ad.log.info('Gets AdbCommandError, change dtim with change_dtim_adb.')
77        change_dtim_adb(ad, gEnableModulatedDTIM)
78        return 0
79
80    ini_file_local = ini_file_phone.split('/')[-1]
81
82    # Pull the file and change the DTIM to desired value
83    ad.adb.pull('{} {}'.format(ini_file_phone, ini_file_local))
84
85    with open(ini_file_local, 'r') as fin:
86        for line in fin:
87            if ENABLED_MODULATED_DTIM in line:
88                gE_old = line.strip('\n')
89                gEDTIM_old = line.strip(ENABLED_MODULATED_DTIM).strip('\n')
90            if MAX_MODULATED_DTIM in line:
91                gM_old = line.strip('\n')
92                gMDTIM_old = line.strip(MAX_MODULATED_DTIM).strip('\n')
93    fin.close()
94    if int(gEDTIM_old) == gEnableModulatedDTIM and int(
95            gMDTIM_old) == gMaxLIModulatedDTIM:
96        ad.log.info('Current DTIM is already the desired value,'
97                    'no need to reset it')
98        return 0
99
100    gE_new = ENABLED_MODULATED_DTIM + str(gEnableModulatedDTIM)
101    gM_new = MAX_MODULATED_DTIM + str(gMaxLIModulatedDTIM)
102
103    sed_gE = 'sed -i \'s/{}/{}/g\' {}'.format(gE_old, gE_new, ini_file_local)
104    sed_gM = 'sed -i \'s/{}/{}/g\' {}'.format(gM_old, gM_new, ini_file_local)
105    job.run(sed_gE)
106    job.run(sed_gM)
107
108    # Push the file to the phone
109    push_file_to_phone(ad, ini_file_local, ini_file_phone)
110    ad.log.info('DTIM changes checked in and rebooting...')
111    ad.reboot()
112    # Wait for auto-wifi feature to start
113    time.sleep(20)
114    ad.adb.shell('dumpsys battery set level 100')
115    ad.log.info('DTIM updated and device back from reboot')
116    return 1
117
118def change_dtim_adb(ad, gEnableModulatedDTIM):
119    """Function to change the DTIM setting in the P21 phone.
120
121        This method should be run after connecting wifi.
122
123    Args:
124        ad: the target android device, AndroidDevice object
125        gEnableModulatedDTIM: Modulated DTIM, int
126    """
127    ad.log.info('Changes DTIM to {} with adb'.format(gEnableModulatedDTIM))
128    ad.adb.root()
129    screen_status = ad.adb.shell('dumpsys nfc | grep Screen')
130    screen_is_on = 'ON_UNLOCKED' in screen_status
131
132    # To read the dtim with 'adb shell wl bcn_li_dtim', the screen should be off
133    if screen_is_on:
134        ad.log.info('The screen is on. Set it to off before change dtim')
135        ad.droid.goToSleepNow()
136        time_limit_seconds = 60
137        _wait_screen_off(ad, time_limit_seconds)
138
139    old_dtim = _read_dtim_adb(ad)
140    ad.log.info('The dtim before change is {}'.format(old_dtim))
141    try:
142        if int(old_dtim) == gEnableModulatedDTIM:
143            ad.log.info('Current DTIM is already the desired value,'
144                        'no need to reset it')
145            if screen_is_on:
146                ad.log.info('Changes the screen to the original on status')
147                ad.droid.wakeUpNow()
148            return
149    except Exception as e:
150        ad.log.info('old_dtim is not available from adb')
151
152    current_dtim = _set_dtim(ad, gEnableModulatedDTIM)
153    ad.log.info(
154        'Old DTIM is {}, current DTIM is {}'.format(old_dtim, current_dtim))
155    if screen_is_on:
156        ad.log.info('Changes the screen to the original on status')
157        ad.droid.wakeUpNow()
158
159def _set_dtim(ad, gEnableModulatedDTIM):
160    out = ad.adb.shell("halutil -dtim_config {}".format(gEnableModulatedDTIM))
161    ad.log.info('set dtim to {}, stdout: {}'.format(
162        gEnableModulatedDTIM, out))
163    return _read_dtim_adb(ad)
164
165def _read_dtim_adb(ad):
166    try:
167        old_dtim = ad.adb.shell('wl bcn_li_dtim')
168        return old_dtim
169    except Exception as e:
170        ad.log.info('When reading dtim get error {}'.format(e))
171        return 'The dtim value is not available from adb'
172
173def _wait_screen_off(ad, time_limit_seconds):
174    while time_limit_seconds > 0:
175        screen_status = ad.adb.shell('dumpsys nfc | grep Screen')
176        if 'OFF_UNLOCKED' in screen_status:
177            ad.log.info('The screen status is {}'.format(screen_status))
178            return
179        time.sleep(1)
180        time_limit_seconds -= 1
181    raise TimeoutError('Timed out while waiting the screen off after {} '
182                       'seconds.'.format(time_limit_seconds))
183
184
185def push_file_to_phone(ad, file_local, file_phone):
186    """Function to push local file to android phone.
187
188    Args:
189        ad: the target android device
190        file_local: the locla file to push
191        file_phone: the file/directory on the phone to be pushed
192    """
193    ad.adb.root()
194    cmd_out = ad.adb.remount()
195    if 'Permission denied' in cmd_out:
196        ad.log.info('Need to disable verity first and reboot')
197        ad.adb.disable_verity()
198        time.sleep(1)
199        ad.reboot()
200        ad.log.info('Verity disabled and device back from reboot')
201        ad.adb.root()
202        ad.adb.remount()
203    time.sleep(1)
204    ad.adb.push('{} {}'.format(file_local, file_phone))
205
206
207def ap_setup(ap, network, bandwidth=80, dtim_period=None):
208    """Set up the whirlwind AP with provided network info.
209
210    Args:
211        ap: access_point object of the AP
212        network: dict with information of the network, including ssid, password
213                 bssid, channel etc.
214        bandwidth: the operation bandwidth for the AP, default 80MHz
215        dtim_period: the dtim period of access point
216    Returns:
217        brconfigs: the bridge interface configs
218    """
219    log = logging.getLogger()
220    bss_settings = []
221    ssid = network[wutils.WifiEnums.SSID_KEY]
222    if "password" in network.keys():
223        password = network["password"]
224        security = hostapd_security.Security(
225            security_mode="wpa", password=password)
226    else:
227        security = hostapd_security.Security(security_mode=None, password=None)
228    channel = network["channel"]
229    config = hostapd_ap_preset.create_ap_preset(
230        channel=channel,
231        ssid=ssid,
232        dtim_period=dtim_period,
233        security=security,
234        bss_settings=bss_settings,
235        vht_bandwidth=bandwidth,
236        profile_name='whirlwind',
237        iface_wlan_2g=ap.wlan_2g,
238        iface_wlan_5g=ap.wlan_5g)
239    config_bridge = ap.generate_bridge_configs(channel)
240    brconfigs = bi.BridgeInterfaceConfigs(config_bridge[0], config_bridge[1],
241                                          config_bridge[2])
242    ap.bridge.startup(brconfigs)
243    ap.start_ap(config)
244    log.info("AP started on channel {} with SSID {}".format(channel, ssid))
245    return brconfigs
246
247
248def run_iperf_client_nonblocking(ad, server_host, extra_args=""):
249    """Start iperf client on the device with nohup.
250
251    Return status as true if iperf client start successfully.
252    And data flow information as results.
253
254    Args:
255        ad: the android device under test
256        server_host: Address of the iperf server.
257        extra_args: A string representing extra arguments for iperf client,
258            e.g. "-i 1 -t 30".
259
260    """
261    log = logging.getLogger()
262    ad.adb.shell_nb("nohup >/dev/null 2>&1 sh -c 'iperf3 -c {} {} &'".format(
263        server_host, extra_args))
264    log.info("IPerf client started")
265
266
267def get_wifi_rssi(ad):
268    """Get the RSSI of the device.
269
270    Args:
271        ad: the android device under test
272    Returns:
273        RSSI: the rssi level of the device
274    """
275    RSSI = ad.droid.wifiGetConnectionInfo()['rssi']
276    return RSSI
277
278
279def get_phone_ip(ad):
280    """Get the WiFi IP address of the phone.
281
282    Args:
283        ad: the android device under test
284    Returns:
285        IP: IP address of the phone for WiFi, as a string
286    """
287    IP = ad.droid.connectivityGetIPv4Addresses('wlan0')[0]
288
289    return IP
290
291
292def get_phone_mac(ad):
293    """Get the WiFi MAC address of the phone.
294
295    Args:
296        ad: the android device under test
297    Returns:
298        mac: MAC address of the phone for WiFi, as a string
299    """
300    mac = ad.droid.wifiGetConnectionInfo()["mac_address"]
301
302    return mac
303
304
305def get_phone_ipv6(ad):
306    """Get the WiFi IPV6 address of the phone.
307
308    Args:
309        ad: the android device under test
310    Returns:
311        IPv6: IPv6 address of the phone for WiFi, as a string
312    """
313    IPv6 = ad.droid.connectivityGetLinkLocalIpv6Address('wlan0')[:-6]
314
315    return IPv6
316
317
318def wait_for_dhcp(interface_name):
319    """Wait the DHCP address assigned to desired interface.
320
321    Getting DHCP address takes time and the wait time isn't constant. Utilizing
322    utils.timeout to keep trying until success
323
324    Args:
325        interface_name: desired interface name
326    Returns:
327        ip: ip address of the desired interface name
328    Raise:
329        TimeoutError: After timeout, if no DHCP assigned, raise
330    """
331    log = logging.getLogger()
332    reset_host_interface(interface_name)
333    start_time = time.time()
334    time_limit_seconds = 60
335    ip = '0.0.0.0'
336    while start_time + time_limit_seconds > time.time():
337        ip = scapy.get_if_addr(interface_name)
338        if ip == '0.0.0.0':
339            time.sleep(1)
340        else:
341            log.info(
342                'DHCP address assigned to %s as %s' % (interface_name, ip))
343            return ip
344    raise TimeoutError('Timed out while getting if_addr after %s seconds.' %
345                       time_limit_seconds)
346
347
348def reset_host_interface(intferface_name):
349    """Reset the host interface.
350
351    Args:
352        intferface_name: the desired interface to reset
353    """
354    log = logging.getLogger()
355    intf_down_cmd = 'ifconfig %s down' % intferface_name
356    intf_up_cmd = 'ifconfig %s up' % intferface_name
357    try:
358        job.run(intf_down_cmd)
359        time.sleep(10)
360        job.run(intf_up_cmd)
361        log.info('{} has been reset'.format(intferface_name))
362    except job.Error:
363        raise Exception('No such interface')
364
365
366def bringdown_host_interface(intferface_name):
367    """Reset the host interface.
368
369    Args:
370        intferface_name: the desired interface to reset
371    """
372    log = logging.getLogger()
373    intf_down_cmd = 'ifconfig %s down' % intferface_name
374    try:
375        job.run(intf_down_cmd)
376        time.sleep(2)
377        log.info('{} has been brought down'.format(intferface_name))
378    except job.Error:
379        raise Exception('No such interface')
380
381
382def create_pkt_config(test_class):
383    """Creates the config for generating multicast packets
384
385    Args:
386        test_class: object with all networking paramters
387
388    Returns:
389        Dictionary with the multicast packet config
390    """
391    addr_type = (scapy.IPV6_ADDR_LINKLOCAL
392                 if test_class.ipv6_src_type == 'LINK_LOCAL' else
393                 scapy.IPV6_ADDR_GLOBAL)
394
395    mac_dst = test_class.mac_dst
396    if GET_FROM_PHONE in test_class.mac_dst:
397        mac_dst = get_phone_mac(test_class.dut)
398
399    ipv4_dst = test_class.ipv4_dst
400    if GET_FROM_PHONE in test_class.ipv4_dst:
401        ipv4_dst = get_phone_ip(test_class.dut)
402
403    ipv6_dst = test_class.ipv6_dst
404    if GET_FROM_PHONE in test_class.ipv6_dst:
405        ipv6_dst = get_phone_ipv6(test_class.dut)
406
407    ipv4_gw = test_class.ipv4_gwt
408    if GET_FROM_AP in test_class.ipv4_gwt:
409        ipv4_gw = test_class.access_point.ssh_settings.hostname
410
411    pkt_gen_config = {
412        'interf': test_class.pkt_sender.interface,
413        'subnet_mask': test_class.sub_mask,
414        'src_mac': test_class.mac_src,
415        'dst_mac': mac_dst,
416        'src_ipv4': test_class.ipv4_src,
417        'dst_ipv4': ipv4_dst,
418        'src_ipv6': test_class.ipv6_src,
419        'src_ipv6_type': addr_type,
420        'dst_ipv6': ipv6_dst,
421        'gw_ipv4': ipv4_gw
422    }
423    return pkt_gen_config
424