1#!/usr/bin/env python3
2#
3#   Copyright 2016 - The Android Open Source Project
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 base64
18import concurrent.futures
19import copy
20import datetime
21import functools
22import ipaddress
23import json
24import logging
25import os
26import random
27import re
28import signal
29import string
30import socket
31import subprocess
32import time
33import threading
34import traceback
35import zipfile
36from concurrent.futures import ThreadPoolExecutor
37
38from acts import signals
39from acts.controllers import adb
40from acts.libs.proc import job
41
42# File name length is limited to 255 chars on some OS, so we need to make sure
43# the file names we output fits within the limit.
44MAX_FILENAME_LEN = 255
45
46
47class ActsUtilsError(Exception):
48    """Generic error raised for exceptions in ACTS utils."""
49
50
51class NexusModelNames:
52    # TODO(angli): This will be fixed later by angli.
53    ONE = 'sprout'
54    N5 = 'hammerhead'
55    N5v2 = 'bullhead'
56    N6 = 'shamu'
57    N6v2 = 'angler'
58    N6v3 = 'marlin'
59    N5v3 = 'sailfish'
60
61
62class DozeModeStatus:
63    ACTIVE = "ACTIVE"
64    IDLE = "IDLE"
65
66
67ascii_letters_and_digits = string.ascii_letters + string.digits
68valid_filename_chars = "-_." + ascii_letters_and_digits
69
70models = ("sprout", "occam", "hammerhead", "bullhead", "razor", "razorg",
71          "shamu", "angler", "volantis", "volantisg", "mantaray", "fugu",
72          "ryu", "marlin", "sailfish")
73
74manufacture_name_to_model = {
75    "flo": "razor",
76    "flo_lte": "razorg",
77    "flounder": "volantis",
78    "flounder_lte": "volantisg",
79    "dragon": "ryu"
80}
81
82GMT_to_olson = {
83    "GMT-9": "America/Anchorage",
84    "GMT-8": "US/Pacific",
85    "GMT-7": "US/Mountain",
86    "GMT-6": "US/Central",
87    "GMT-5": "US/Eastern",
88    "GMT-4": "America/Barbados",
89    "GMT-3": "America/Buenos_Aires",
90    "GMT-2": "Atlantic/South_Georgia",
91    "GMT-1": "Atlantic/Azores",
92    "GMT+0": "Africa/Casablanca",
93    "GMT+1": "Europe/Amsterdam",
94    "GMT+2": "Europe/Athens",
95    "GMT+3": "Europe/Moscow",
96    "GMT+4": "Asia/Baku",
97    "GMT+5": "Asia/Oral",
98    "GMT+6": "Asia/Almaty",
99    "GMT+7": "Asia/Bangkok",
100    "GMT+8": "Asia/Hong_Kong",
101    "GMT+9": "Asia/Tokyo",
102    "GMT+10": "Pacific/Guam",
103    "GMT+11": "Pacific/Noumea",
104    "GMT+12": "Pacific/Fiji",
105    "GMT+13": "Pacific/Tongatapu",
106    "GMT-11": "Pacific/Midway",
107    "GMT-10": "Pacific/Honolulu"
108}
109
110
111def abs_path(path):
112    """Resolve the '.' and '~' in a path to get the absolute path.
113
114    Args:
115        path: The path to expand.
116
117    Returns:
118        The absolute path of the input path.
119    """
120    return os.path.abspath(os.path.expanduser(path))
121
122
123def get_current_epoch_time():
124    """Current epoch time in milliseconds.
125
126    Returns:
127        An integer representing the current epoch time in milliseconds.
128    """
129    return int(round(time.time() * 1000))
130
131
132def get_current_human_time():
133    """Returns the current time in human readable format.
134
135    Returns:
136        The current time stamp in Month-Day-Year Hour:Min:Sec format.
137    """
138    return time.strftime("%m-%d-%Y %H:%M:%S ")
139
140
141def epoch_to_human_time(epoch_time):
142    """Converts an epoch timestamp to human readable time.
143
144    This essentially converts an output of get_current_epoch_time to an output
145    of get_current_human_time
146
147    Args:
148        epoch_time: An integer representing an epoch timestamp in milliseconds.
149
150    Returns:
151        A time string representing the input time.
152        None if input param is invalid.
153    """
154    if isinstance(epoch_time, int):
155        try:
156            d = datetime.datetime.fromtimestamp(epoch_time / 1000)
157            return d.strftime("%m-%d-%Y %H:%M:%S ")
158        except ValueError:
159            return None
160
161
162def get_timezone_olson_id():
163    """Return the Olson ID of the local (non-DST) timezone.
164
165    Returns:
166        A string representing one of the Olson IDs of the local (non-DST)
167        timezone.
168    """
169    tzoffset = int(time.timezone / 3600)
170    gmt = None
171    if tzoffset <= 0:
172        gmt = "GMT+{}".format(-tzoffset)
173    else:
174        gmt = "GMT-{}".format(tzoffset)
175    return GMT_to_olson[gmt]
176
177
178def get_next_device(test_bed_controllers, used_devices):
179    """Gets the next device in a list of testbed controllers
180
181    Args:
182        test_bed_controllers: A list of testbed controllers of a particular
183            type, for example a list ACTS Android devices.
184        used_devices: A list of devices that have been used.  This can be a
185            mix of devices, for example a fuchsia device and an Android device.
186    Returns:
187        The next device in the test_bed_controllers list or None if there are
188        no items that are not in the used devices list.
189    """
190    if test_bed_controllers:
191        device_list = test_bed_controllers
192    else:
193        raise ValueError('test_bed_controllers is empty.')
194    for used_device in used_devices:
195        if used_device in device_list:
196            device_list.remove(used_device)
197    if device_list:
198        return device_list[0]
199    else:
200        return None
201
202
203def find_files(paths, file_predicate):
204    """Locate files whose names and extensions match the given predicate in
205    the specified directories.
206
207    Args:
208        paths: A list of directory paths where to find the files.
209        file_predicate: A function that returns True if the file name and
210          extension are desired.
211
212    Returns:
213        A list of files that match the predicate.
214    """
215    file_list = []
216    if not isinstance(paths, list):
217        paths = [paths]
218    for path in paths:
219        p = abs_path(path)
220        for dirPath, subdirList, fileList in os.walk(p):
221            for fname in fileList:
222                name, ext = os.path.splitext(fname)
223                if file_predicate(name, ext):
224                    file_list.append((dirPath, name, ext))
225    return file_list
226
227
228def load_config(file_full_path, log_errors=True):
229    """Loads a JSON config file.
230
231    Returns:
232        A JSON object.
233    """
234    with open(file_full_path, 'r') as f:
235        try:
236            return json.load(f)
237        except Exception as e:
238            if log_errors:
239                logging.error("Exception error to load %s: %s", f, e)
240            raise
241
242
243def load_file_to_base64_str(f_path):
244    """Loads the content of a file into a base64 string.
245
246    Args:
247        f_path: full path to the file including the file name.
248
249    Returns:
250        A base64 string representing the content of the file in utf-8 encoding.
251    """
252    path = abs_path(f_path)
253    with open(path, 'rb') as f:
254        f_bytes = f.read()
255        base64_str = base64.b64encode(f_bytes).decode("utf-8")
256        return base64_str
257
258
259def dump_string_to_file(content, file_path, mode='w'):
260    """ Dump content of a string to
261
262    Args:
263        content: content to be dumped to file
264        file_path: full path to the file including the file name.
265        mode: file open mode, 'w' (truncating file) by default
266    :return:
267    """
268    full_path = abs_path(file_path)
269    with open(full_path, mode) as f:
270        f.write(content)
271
272
273def list_of_dict_to_dict_of_dict(list_of_dicts, dict_key):
274    """Transforms a list of dicts to a dict of dicts.
275
276    For instance:
277    >>> list_of_dict_to_dict_of_dict([{'a': '1', 'b':'2'},
278    >>>                               {'a': '3', 'b':'4'}],
279    >>>                              'b')
280
281    returns:
282
283    >>> {'2': {'a': '1', 'b':'2'},
284    >>>  '4': {'a': '3', 'b':'4'}}
285
286    Args:
287        list_of_dicts: A list of dictionaries.
288        dict_key: The key in the inner dict to be used as the key for the
289                  outer dict.
290    Returns:
291        A dict of dicts.
292    """
293    return {d[dict_key]: d for d in list_of_dicts}
294
295
296def dict_purge_key_if_value_is_none(dictionary):
297    """Removes all pairs with value None from dictionary."""
298    for k, v in dict(dictionary).items():
299        if v is None:
300            del dictionary[k]
301    return dictionary
302
303
304def find_field(item_list, cond, comparator, target_field):
305    """Finds the value of a field in a dict object that satisfies certain
306    conditions.
307
308    Args:
309        item_list: A list of dict objects.
310        cond: A param that defines the condition.
311        comparator: A function that checks if an dict satisfies the condition.
312        target_field: Name of the field whose value to be returned if an item
313            satisfies the condition.
314
315    Returns:
316        Target value or None if no item satisfies the condition.
317    """
318    for item in item_list:
319        if comparator(item, cond) and target_field in item:
320            return item[target_field]
321    return None
322
323
324def rand_ascii_str(length):
325    """Generates a random string of specified length, composed of ascii letters
326    and digits.
327
328    Args:
329        length: The number of characters in the string.
330
331    Returns:
332        The random string generated.
333    """
334    letters = [random.choice(ascii_letters_and_digits) for i in range(length)]
335    return ''.join(letters)
336
337
338def rand_hex_str(length):
339    """Generates a random string of specified length, composed of hex digits
340
341    Args:
342        length: The number of characters in the string.
343
344    Returns:
345        The random string generated.
346    """
347    letters = [random.choice(string.hexdigits) for i in range(length)]
348    return ''.join(letters)
349
350
351# Thead/Process related functions.
352def concurrent_exec(func, param_list):
353    """Executes a function with different parameters pseudo-concurrently.
354
355    This is basically a map function. Each element (should be an iterable) in
356    the param_list is unpacked and passed into the function. Due to Python's
357    GIL, there's no true concurrency. This is suited for IO-bound tasks.
358
359    Args:
360        func: The function that parforms a task.
361        param_list: A list of iterables, each being a set of params to be
362            passed into the function.
363
364    Returns:
365        A list of return values from each function execution. If an execution
366        caused an exception, the exception object will be the corresponding
367        result.
368    """
369    with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor:
370        # Start the load operations and mark each future with its params
371        future_to_params = {executor.submit(func, *p): p for p in param_list}
372        return_vals = []
373        for future in concurrent.futures.as_completed(future_to_params):
374            params = future_to_params[future]
375            try:
376                return_vals.append(future.result())
377            except Exception as exc:
378                print("{} generated an exception: {}".format(
379                    params, traceback.format_exc()))
380                return_vals.append(exc)
381        return return_vals
382
383
384def exe_cmd(*cmds):
385    """Executes commands in a new shell.
386
387    Args:
388        cmds: A sequence of commands and arguments.
389
390    Returns:
391        The output of the command run.
392
393    Raises:
394        OSError is raised if an error occurred during the command execution.
395    """
396    cmd = ' '.join(cmds)
397    proc = subprocess.Popen(cmd,
398                            stdout=subprocess.PIPE,
399                            stderr=subprocess.PIPE,
400                            shell=True)
401    (out, err) = proc.communicate()
402    if not err:
403        return out
404    raise OSError(err)
405
406
407def require_sl4a(android_devices):
408    """Makes sure sl4a connection is established on the given AndroidDevice
409    objects.
410
411    Args:
412        android_devices: A list of AndroidDevice objects.
413
414    Raises:
415        AssertionError is raised if any given android device does not have SL4A
416        connection established.
417    """
418    for ad in android_devices:
419        msg = "SL4A connection not established properly on %s." % ad.serial
420        assert ad.droid, msg
421
422
423def _assert_subprocess_running(proc):
424    """Checks if a subprocess has terminated on its own.
425
426    Args:
427        proc: A subprocess returned by subprocess.Popen.
428
429    Raises:
430        ActsUtilsError is raised if the subprocess has stopped.
431    """
432    ret = proc.poll()
433    if ret is not None:
434        out, err = proc.communicate()
435        raise ActsUtilsError("Process %d has terminated. ret: %d, stderr: %s,"
436                             " stdout: %s" % (proc.pid, ret, err, out))
437
438
439def start_standing_subprocess(cmd, check_health_delay=0, shell=True):
440    """Starts a long-running subprocess.
441
442    This is not a blocking call and the subprocess started by it should be
443    explicitly terminated with stop_standing_subprocess.
444
445    For short-running commands, you should use exe_cmd, which blocks.
446
447    You can specify a health check after the subprocess is started to make sure
448    it did not stop prematurely.
449
450    Args:
451        cmd: string, the command to start the subprocess with.
452        check_health_delay: float, the number of seconds to wait after the
453                            subprocess starts to check its health. Default is 0,
454                            which means no check.
455
456    Returns:
457        The subprocess that got started.
458    """
459    proc = subprocess.Popen(cmd,
460                            stdout=subprocess.PIPE,
461                            stderr=subprocess.PIPE,
462                            shell=shell,
463                            preexec_fn=os.setpgrp)
464    logging.debug("Start standing subprocess with cmd: %s", cmd)
465    if check_health_delay > 0:
466        time.sleep(check_health_delay)
467        _assert_subprocess_running(proc)
468    return proc
469
470
471def stop_standing_subprocess(proc, kill_signal=signal.SIGTERM):
472    """Stops a subprocess started by start_standing_subprocess.
473
474    Before killing the process, we check if the process is running, if it has
475    terminated, ActsUtilsError is raised.
476
477    Catches and ignores the PermissionError which only happens on Macs.
478
479    Args:
480        proc: Subprocess to terminate.
481    """
482    pid = proc.pid
483    logging.debug("Stop standing subprocess %d", pid)
484    _assert_subprocess_running(proc)
485    try:
486        os.killpg(pid, kill_signal)
487    except PermissionError:
488        pass
489
490
491def wait_for_standing_subprocess(proc, timeout=None):
492    """Waits for a subprocess started by start_standing_subprocess to finish
493    or times out.
494
495    Propagates the exception raised by the subprocess.wait(.) function.
496    The subprocess.TimeoutExpired exception is raised if the process timed-out
497    rather then terminating.
498
499    If no exception is raised: the subprocess terminated on its own. No need
500    to call stop_standing_subprocess() to kill it.
501
502    If an exception is raised: the subprocess is still alive - it did not
503    terminate. Either call stop_standing_subprocess() to kill it, or call
504    wait_for_standing_subprocess() to keep waiting for it to terminate on its
505    own.
506
507    Args:
508        p: Subprocess to wait for.
509        timeout: An integer number of seconds to wait before timing out.
510    """
511    proc.wait(timeout)
512
513
514def sync_device_time(ad):
515    """Sync the time of an android device with the current system time.
516
517    Both epoch time and the timezone will be synced.
518
519    Args:
520        ad: The android device to sync time on.
521    """
522    ad.adb.shell("settings put global auto_time 0", ignore_status=True)
523    ad.adb.shell("settings put global auto_time_zone 0", ignore_status=True)
524    droid = ad.droid
525    droid.setTimeZone(get_timezone_olson_id())
526    droid.setTime(get_current_epoch_time())
527
528
529# Timeout decorator block
530class TimeoutError(Exception):
531    """Exception for timeout decorator related errors.
532    """
533    pass
534
535
536def _timeout_handler(signum, frame):
537    """Handler function used by signal to terminate a timed out function.
538    """
539    raise TimeoutError()
540
541
542def timeout(sec):
543    """A decorator used to add time out check to a function.
544
545    This only works in main thread due to its dependency on signal module.
546    Do NOT use it if the decorated funtion does not run in the Main thread.
547
548    Args:
549        sec: Number of seconds to wait before the function times out.
550            No timeout if set to 0
551
552    Returns:
553        What the decorated function returns.
554
555    Raises:
556        TimeoutError is raised when time out happens.
557    """
558    def decorator(func):
559        @functools.wraps(func)
560        def wrapper(*args, **kwargs):
561            if sec:
562                signal.signal(signal.SIGALRM, _timeout_handler)
563                signal.alarm(sec)
564            try:
565                return func(*args, **kwargs)
566            except TimeoutError:
567                raise TimeoutError(("Function {} timed out after {} "
568                                    "seconds.").format(func.__name__, sec))
569            finally:
570                signal.alarm(0)
571
572        return wrapper
573
574    return decorator
575
576
577def trim_model_name(model):
578    """Trim any prefix and postfix and return the android designation of the
579    model name.
580
581    e.g. "m_shamu" will be trimmed to "shamu".
582
583    Args:
584        model: model name to be trimmed.
585
586    Returns
587        Trimmed model name if one of the known model names is found.
588        None otherwise.
589    """
590    # Directly look up first.
591    if model in models:
592        return model
593    if model in manufacture_name_to_model:
594        return manufacture_name_to_model[model]
595    # If not found, try trimming off prefix/postfix and look up again.
596    tokens = re.split("_|-", model)
597    for t in tokens:
598        if t in models:
599            return t
600        if t in manufacture_name_to_model:
601            return manufacture_name_to_model[t]
602    return None
603
604
605def force_airplane_mode(ad, new_state, timeout_value=60):
606    """Force the device to set airplane mode on or off by adb shell command.
607
608    Args:
609        ad: android device object.
610        new_state: Turn on airplane mode if True.
611            Turn off airplane mode if False.
612        timeout_value: max wait time for 'adb wait-for-device'
613
614    Returns:
615        True if success.
616        False if timeout.
617    """
618
619    # Using timeout decorator.
620    # Wait for device with timeout. If after <timeout_value> seconds, adb
621    # is still waiting for device, throw TimeoutError exception.
622    @timeout(timeout_value)
623    def wait_for_device_with_timeout(ad):
624        ad.adb.wait_for_device()
625
626    try:
627        wait_for_device_with_timeout(ad)
628        ad.adb.shell("settings put global airplane_mode_on {}".format(
629            1 if new_state else 0))
630        ad.adb.shell("am broadcast -a android.intent.action.AIRPLANE_MODE")
631    except TimeoutError:
632        # adb wait for device timeout
633        return False
634    return True
635
636
637def get_battery_level(ad):
638    """Gets battery level from device
639
640    Returns:
641        battery_level: int indicating battery level
642    """
643    output = ad.adb.shell("dumpsys battery")
644    match = re.search(r"level: (?P<battery_level>\S+)", output)
645    battery_level = int(match.group("battery_level"))
646    return battery_level
647
648
649def get_device_usb_charging_status(ad):
650    """ Returns the usb charging status of the device.
651
652    Args:
653        ad: android device object
654
655    Returns:
656        True if charging
657        False if not charging
658     """
659    adb_shell_result = ad.adb.shell("dumpsys deviceidle get charging")
660    ad.log.info("Device Charging State: {}".format(adb_shell_result))
661    return adb_shell_result == 'true'
662
663
664def disable_usb_charging(ad):
665    """ Unplug device from usb charging.
666
667    Args:
668        ad: android device object
669
670    Returns:
671        True if device is unplugged
672        False otherwise
673    """
674    ad.adb.shell("dumpsys battery unplug")
675    if not get_device_usb_charging_status(ad):
676        return True
677    else:
678        ad.log.info("Could not disable USB charging")
679        return False
680
681
682def enable_usb_charging(ad):
683    """ Plug device to usb charging.
684
685    Args:
686        ad: android device object
687
688    Returns:
689        True if device is Plugged
690        False otherwise
691    """
692    ad.adb.shell("dumpsys battery reset")
693    if get_device_usb_charging_status(ad):
694        return True
695    else:
696        ad.log.info("Could not enable USB charging")
697        return False
698
699
700def enable_doze(ad):
701    """Force the device into doze mode.
702
703    Args:
704        ad: android device object.
705
706    Returns:
707        True if device is in doze mode.
708        False otherwise.
709    """
710    ad.adb.shell("dumpsys battery unplug")
711    ad.adb.shell("dumpsys deviceidle enable")
712    ad.adb.shell("dumpsys deviceidle force-idle")
713    ad.droid.goToSleepNow()
714    time.sleep(5)
715    adb_shell_result = ad.adb.shell("dumpsys deviceidle get deep")
716    if not adb_shell_result.startswith(DozeModeStatus.IDLE):
717        info = ("dumpsys deviceidle get deep: {}".format(adb_shell_result))
718        print(info)
719        return False
720    return True
721
722
723def disable_doze(ad):
724    """Force the device not in doze mode.
725
726    Args:
727        ad: android device object.
728
729    Returns:
730        True if device is not in doze mode.
731        False otherwise.
732    """
733    ad.adb.shell("dumpsys deviceidle disable")
734    ad.adb.shell("dumpsys battery reset")
735    adb_shell_result = ad.adb.shell("dumpsys deviceidle get deep")
736    if not adb_shell_result.startswith(DozeModeStatus.ACTIVE):
737        info = ("dumpsys deviceidle get deep: {}".format(adb_shell_result))
738        print(info)
739        return False
740    return True
741
742
743def enable_doze_light(ad):
744    """Force the device into doze light mode.
745
746    Args:
747        ad: android device object.
748
749    Returns:
750        True if device is in doze light mode.
751        False otherwise.
752    """
753    ad.adb.shell("dumpsys battery unplug")
754    ad.droid.goToSleepNow()
755    time.sleep(5)
756    ad.adb.shell("cmd deviceidle enable light")
757    ad.adb.shell("cmd deviceidle step light")
758    adb_shell_result = ad.adb.shell("dumpsys deviceidle get light")
759    if not adb_shell_result.startswith(DozeModeStatus.IDLE):
760        info = ("dumpsys deviceidle get light: {}".format(adb_shell_result))
761        print(info)
762        return False
763    return True
764
765
766def disable_doze_light(ad):
767    """Force the device not in doze light mode.
768
769    Args:
770        ad: android device object.
771
772    Returns:
773        True if device is not in doze light mode.
774        False otherwise.
775    """
776    ad.adb.shell("dumpsys battery reset")
777    ad.adb.shell("cmd deviceidle disable light")
778    adb_shell_result = ad.adb.shell("dumpsys deviceidle get light")
779    if not adb_shell_result.startswith(DozeModeStatus.ACTIVE):
780        info = ("dumpsys deviceidle get light: {}".format(adb_shell_result))
781        print(info)
782        return False
783    return True
784
785
786def set_ambient_display(ad, new_state):
787    """Set "Ambient Display" in Settings->Display
788
789    Args:
790        ad: android device object.
791        new_state: new state for "Ambient Display". True or False.
792    """
793    ad.adb.shell(
794        "settings put secure doze_enabled {}".format(1 if new_state else 0))
795
796
797def set_adaptive_brightness(ad, new_state):
798    """Set "Adaptive Brightness" in Settings->Display
799
800    Args:
801        ad: android device object.
802        new_state: new state for "Adaptive Brightness". True or False.
803    """
804    ad.adb.shell("settings put system screen_brightness_mode {}".format(
805        1 if new_state else 0))
806
807
808def set_auto_rotate(ad, new_state):
809    """Set "Auto-rotate" in QuickSetting
810
811    Args:
812        ad: android device object.
813        new_state: new state for "Auto-rotate". True or False.
814    """
815    ad.adb.shell("settings put system accelerometer_rotation {}".format(
816        1 if new_state else 0))
817
818
819def set_location_service(ad, new_state):
820    """Set Location service on/off in Settings->Location
821
822    Args:
823        ad: android device object.
824        new_state: new state for "Location service".
825            If new_state is False, turn off location service.
826            If new_state if True, set location service to "High accuracy".
827    """
828    ad.adb.shell("content insert --uri "
829                 " content://com.google.settings/partner --bind "
830                 "name:s:network_location_opt_in --bind value:s:1")
831    ad.adb.shell("content insert --uri "
832                 " content://com.google.settings/partner --bind "
833                 "name:s:use_location_for_services --bind value:s:1")
834    if new_state:
835        ad.adb.shell("settings put secure location_mode 3")
836    else:
837        ad.adb.shell("settings put secure location_mode 0")
838
839
840def set_mobile_data_always_on(ad, new_state):
841    """Set Mobile_Data_Always_On feature bit
842
843    Args:
844        ad: android device object.
845        new_state: new state for "mobile_data_always_on"
846            if new_state is False, set mobile_data_always_on disabled.
847            if new_state if True, set mobile_data_always_on enabled.
848    """
849    ad.adb.shell("settings put global mobile_data_always_on {}".format(
850        1 if new_state else 0))
851
852
853def bypass_setup_wizard(ad):
854    """Bypass the setup wizard on an input Android device
855
856    Args:
857        ad: android device object.
858
859    Returns:
860        True if Android device successfully bypassed the setup wizard.
861        False if failed.
862    """
863    try:
864        ad.adb.shell("am start -n \"com.google.android.setupwizard/"
865                     ".SetupWizardExitActivity\"")
866        logging.debug("No error during default bypass call.")
867    except adb.AdbError as adb_error:
868        if adb_error.stdout == "ADB_CMD_OUTPUT:0":
869            if adb_error.stderr and \
870                    not adb_error.stderr.startswith("Error type 3\n"):
871                logging.error("ADB_CMD_OUTPUT:0, but error is %s " %
872                              adb_error.stderr)
873                raise adb_error
874            logging.debug("Bypass wizard call received harmless error 3: "
875                          "No setup to bypass.")
876        elif adb_error.stdout == "ADB_CMD_OUTPUT:255":
877            # Run it again as root.
878            ad.adb.root_adb()
879            logging.debug("Need root access to bypass setup wizard.")
880            try:
881                ad.adb.shell("am start -n \"com.google.android.setupwizard/"
882                             ".SetupWizardExitActivity\"")
883                logging.debug("No error during rooted bypass call.")
884            except adb.AdbError as adb_error:
885                if adb_error.stdout == "ADB_CMD_OUTPUT:0":
886                    if adb_error.stderr and \
887                            not adb_error.stderr.startswith("Error type 3\n"):
888                        logging.error("Rooted ADB_CMD_OUTPUT:0, but error is "
889                                      "%s " % adb_error.stderr)
890                        raise adb_error
891                    logging.debug(
892                        "Rooted bypass wizard call received harmless "
893                        "error 3: No setup to bypass.")
894
895    # magical sleep to wait for the gservices override broadcast to complete
896    time.sleep(3)
897
898    provisioned_state = int(
899        ad.adb.shell("settings get global device_provisioned"))
900    if provisioned_state != 1:
901        logging.error("Failed to bypass setup wizard.")
902        return False
903    logging.debug("Setup wizard successfully bypassed.")
904    return True
905
906
907def parse_ping_ouput(ad, count, out, loss_tolerance=20):
908    """Ping Parsing util.
909
910    Args:
911        ad: Android Device Object.
912        count: Number of ICMP packets sent
913        out: shell output text of ping operation
914        loss_tolerance: Threshold after which flag test as false
915    Returns:
916        False: if packet loss is more than loss_tolerance%
917        True: if all good
918    """
919    result = re.search(
920        r"(\d+) packets transmitted, (\d+) received, (\d+)% packet loss", out)
921    if not result:
922        ad.log.info("Ping failed with %s", out)
923        return False
924
925    packet_loss = int(result.group(3))
926    packet_xmit = int(result.group(1))
927    packet_rcvd = int(result.group(2))
928    min_packet_xmit_rcvd = (100 - loss_tolerance) * 0.01
929    if (packet_loss > loss_tolerance
930            or packet_xmit < count * min_packet_xmit_rcvd
931            or packet_rcvd < count * min_packet_xmit_rcvd):
932        ad.log.error("%s, ping failed with loss more than tolerance %s%%",
933                     result.group(0), loss_tolerance)
934        return False
935    ad.log.info("Ping succeed with %s", result.group(0))
936    return True
937
938
939def adb_shell_ping(ad,
940                   count=120,
941                   dest_ip="www.google.com",
942                   timeout=200,
943                   loss_tolerance=20):
944    """Ping utility using adb shell.
945
946    Args:
947        ad: Android Device Object.
948        count: Number of ICMP packets to send
949        dest_ip: hostname or IP address
950                 default www.google.com
951        timeout: timeout for icmp pings to complete.
952    """
953    ping_cmd = "ping -W 1"
954    if count:
955        ping_cmd += " -c %d" % count
956    if dest_ip:
957        ping_cmd += " %s" % dest_ip
958    try:
959        ad.log.info("Starting ping test to %s using adb command %s", dest_ip,
960                    ping_cmd)
961        out = ad.adb.shell(ping_cmd, timeout=timeout, ignore_status=True)
962        if not parse_ping_ouput(ad, count, out, loss_tolerance):
963            return False
964        return True
965    except Exception as e:
966        ad.log.warning("Ping Test to %s failed with exception %s", dest_ip, e)
967        return False
968
969
970def unzip_maintain_permissions(zip_path, extract_location):
971    """Unzip a .zip file while maintaining permissions.
972
973    Args:
974        zip_path: The path to the zipped file.
975        extract_location: the directory to extract to.
976    """
977    with zipfile.ZipFile(zip_path, 'r') as zip_file:
978        for info in zip_file.infolist():
979            _extract_file(zip_file, info, extract_location)
980
981
982def _extract_file(zip_file, zip_info, extract_location):
983    """Extracts a single entry from a ZipFile while maintaining permissions.
984
985    Args:
986        zip_file: A zipfile.ZipFile.
987        zip_info: A ZipInfo object from zip_file.
988        extract_location: The directory to extract to.
989    """
990    out_path = zip_file.extract(zip_info.filename, path=extract_location)
991    perm = zip_info.external_attr >> 16
992    os.chmod(out_path, perm)
993
994
995def get_directory_size(path):
996    """Computes the total size of the files in a directory, including subdirectories.
997
998    Args:
999        path: The path of the directory.
1000    Returns:
1001        The size of the provided directory.
1002    """
1003    total = 0
1004    for dirpath, dirnames, filenames in os.walk(path):
1005        for filename in filenames:
1006            total += os.path.getsize(os.path.join(dirpath, filename))
1007    return total
1008
1009
1010def get_command_uptime(command_regex):
1011    """Returns the uptime for a given command.
1012
1013    Args:
1014        command_regex: A regex that matches the command line given. Must be
1015            pgrep compatible.
1016    """
1017    pid = job.run('pgrep -f %s' % command_regex).stdout
1018    runtime = ''
1019    if pid:
1020        runtime = job.run('ps -o etime= -p "%s"' % pid).stdout
1021    return runtime
1022
1023
1024def get_process_uptime(process):
1025    """Returns the runtime in [[dd-]hh:]mm:ss, or '' if not running."""
1026    pid = job.run('pidof %s' % process, ignore_status=True).stdout
1027    runtime = ''
1028    if pid:
1029        runtime = job.run('ps -o etime= -p "%s"' % pid).stdout
1030    return runtime
1031
1032
1033def get_device_process_uptime(adb, process):
1034    """Returns the uptime of a device process."""
1035    pid = adb.shell('pidof %s' % process, ignore_status=True)
1036    runtime = ''
1037    if pid:
1038        runtime = adb.shell('ps -o etime= -p "%s"' % pid)
1039    return runtime
1040
1041
1042def wait_until(func, timeout_s, condition=True, sleep_s=1.0):
1043    """Executes a function repeatedly until condition is met.
1044
1045    Args:
1046      func: The function pointer to execute.
1047      timeout_s: Amount of time (in seconds) to wait before raising an
1048                 exception.
1049      condition: The ending condition of the WaitUntil loop.
1050      sleep_s: The amount of time (in seconds) to sleep between each function
1051               execution.
1052
1053    Returns:
1054      The time in seconds before detecting a successful condition.
1055
1056    Raises:
1057      TimeoutError: If the condition was never met and timeout is hit.
1058    """
1059    start_time = time.time()
1060    end_time = start_time + timeout_s
1061    count = 0
1062    while True:
1063        count += 1
1064        if func() == condition:
1065            return time.time() - start_time
1066        if time.time() > end_time:
1067            break
1068        time.sleep(sleep_s)
1069    raise TimeoutError('Failed to complete function %s in %d seconds having '
1070                       'attempted %d times.' % (str(func), timeout_s, count))
1071
1072
1073# Adapted from
1074# https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Python
1075# Available under the Creative Commons Attribution-ShareAlike License
1076def levenshtein(string1, string2):
1077    """Returns the Levenshtein distance of two strings.
1078    Uses Dynamic Programming approach, only keeping track of
1079    two rows of the DP table at a time.
1080
1081    Args:
1082      string1: String to compare to string2
1083      string2: String to compare to string1
1084
1085    Returns:
1086      distance: the Levenshtein distance between string1 and string2
1087    """
1088
1089    if len(string1) < len(string2):
1090        return levenshtein(string2, string1)
1091
1092    if len(string2) == 0:
1093        return len(string1)
1094
1095    previous_row = range(len(string2) + 1)
1096    for i, char1 in enumerate(string1):
1097        current_row = [i + 1]
1098        for j, char2 in enumerate(string2):
1099            insertions = previous_row[j + 1] + 1
1100            deletions = current_row[j] + 1
1101            substitutions = previous_row[j] + (char1 != char2)
1102            current_row.append(min(insertions, deletions, substitutions))
1103        previous_row = current_row
1104
1105    return previous_row[-1]
1106
1107
1108def string_similarity(s1, s2):
1109    """Returns a similarity measurement based on Levenshtein distance.
1110
1111    Args:
1112      s1: the string to compare to s2
1113      s2: the string to compare to s1
1114
1115    Returns:
1116      result: the similarity metric
1117    """
1118    lev = levenshtein(s1, s2)
1119    try:
1120        lev_ratio = float(lev) / max(len(s1), len(s2))
1121        result = (1.0 - lev_ratio) * 100
1122    except ZeroDivisionError:
1123        result = 100 if not s2 else 0
1124    return float(result)
1125
1126
1127def run_concurrent_actions_no_raise(*calls):
1128    """Concurrently runs all callables passed in using multithreading.
1129
1130    Example:
1131
1132    >>> def test_function_1(arg1, arg2):
1133    >>>     return arg1, arg2
1134    >>>
1135    >>> def test_function_2(arg1, kwarg='kwarg'):
1136    >>>     raise arg1(kwarg)
1137    >>>
1138    >>> run_concurrent_actions_no_raise(
1139    >>>     lambda: test_function_1('arg1', 'arg2'),
1140    >>>     lambda: test_function_2(IndexError, kwarg='kwarg'),
1141    >>> )
1142    >>> # Output:
1143    >>> [('arg1', 'arg2'), IndexError('kwarg')]
1144
1145    Args:
1146        *calls: A *args list of argumentless callable objects to be called. Note
1147            that if a function has arguments it can be turned into an
1148            argumentless function via the lambda keyword or functools.partial.
1149
1150    Returns:
1151        An array of the returned values or exceptions received from calls,
1152        respective of the order given.
1153    """
1154    with ThreadPoolExecutor(max_workers=len(calls)) as executor:
1155        futures = [executor.submit(call) for call in calls]
1156
1157    results = []
1158    for future in futures:
1159        try:
1160            results.append(future.result())
1161        except Exception as e:
1162            results.append(e)
1163    return results
1164
1165
1166def run_concurrent_actions(*calls):
1167    """Runs all callables passed in concurrently using multithreading.
1168
1169    Examples:
1170
1171    >>> def test_function_1(arg1, arg2):
1172    >>>     print(arg1, arg2)
1173    >>>
1174    >>> def test_function_2(arg1, kwarg='kwarg'):
1175    >>>     raise arg1(kwarg)
1176    >>>
1177    >>> run_concurrent_actions(
1178    >>>     lambda: test_function_1('arg1', 'arg2'),
1179    >>>     lambda: test_function_2(IndexError, kwarg='kwarg'),
1180    >>> )
1181    >>> 'The above line raises IndexError("kwarg")'
1182
1183    Args:
1184        *calls: A *args list of argumentless callable objects to be called. Note
1185            that if a function has arguments it can be turned into an
1186            argumentless function via the lambda keyword or functools.partial.
1187
1188    Returns:
1189        An array of the returned values respective of the order of the calls
1190        argument.
1191
1192    Raises:
1193        If an exception is raised in any of the calls, the first exception
1194        caught will be raised.
1195    """
1196    first_exception = None
1197
1198    class WrappedException(Exception):
1199        """Raised when a passed-in callable raises an exception."""
1200
1201    def call_wrapper(call):
1202        nonlocal first_exception
1203
1204        try:
1205            return call()
1206        except Exception as e:
1207            logging.exception(e)
1208            # Note that there is a potential race condition between two
1209            # exceptions setting first_exception. Even if a locking mechanism
1210            # was added to prevent this from happening, it is still possible
1211            # that we capture the second exception as the first exception, as
1212            # the active thread can swap to the thread that raises the second
1213            # exception. There is no way to solve this with the tools we have
1214            # here, so we do not bother. The effects this issue has on the
1215            # system as a whole are negligible.
1216            if first_exception is None:
1217                first_exception = e
1218            raise WrappedException(e)
1219
1220    with ThreadPoolExecutor(max_workers=len(calls)) as executor:
1221        futures = [executor.submit(call_wrapper, call) for call in calls]
1222
1223    results = []
1224    for future in futures:
1225        try:
1226            results.append(future.result())
1227        except WrappedException:
1228            # We do not need to raise here, since first_exception will already
1229            # be set to the first exception raised by these callables.
1230            break
1231
1232    if first_exception:
1233        raise first_exception
1234
1235    return results
1236
1237
1238def test_concurrent_actions(*calls, failure_exceptions=(Exception, )):
1239    """Concurrently runs all passed in calls using multithreading.
1240
1241    If any callable raises an Exception found within failure_exceptions, the
1242    test case is marked as a failure.
1243
1244    Example:
1245    >>> def test_function_1(arg1, arg2):
1246    >>>     print(arg1, arg2)
1247    >>>
1248    >>> def test_function_2(kwarg='kwarg'):
1249    >>>     raise IndexError(kwarg)
1250    >>>
1251    >>> test_concurrent_actions(
1252    >>>     lambda: test_function_1('arg1', 'arg2'),
1253    >>>     lambda: test_function_2(kwarg='kwarg'),
1254    >>>     failure_exceptions=IndexError
1255    >>> )
1256    >>> 'raises signals.TestFailure due to IndexError being raised.'
1257
1258    Args:
1259        *calls: A *args list of argumentless callable objects to be called. Note
1260            that if a function has arguments it can be turned into an
1261            argumentless function via the lambda keyword or functools.partial.
1262        failure_exceptions: A tuple of all possible Exceptions that will mark
1263            the test as a FAILURE. Any exception that is not in this list will
1264            mark the tests as UNKNOWN.
1265
1266    Returns:
1267        An array of the returned values respective of the order of the calls
1268        argument.
1269
1270    Raises:
1271        signals.TestFailure if any call raises an Exception.
1272    """
1273    try:
1274        return run_concurrent_actions(*calls)
1275    except signals.TestFailure:
1276        # Do not modify incoming test failures
1277        raise
1278    except failure_exceptions as e:
1279        raise signals.TestFailure(e)
1280
1281
1282class SuppressLogOutput(object):
1283    """Context manager used to suppress all logging output for the specified
1284    logger and level(s).
1285    """
1286    def __init__(self, logger=logging.getLogger(), log_levels=None):
1287        """Create a SuppressLogOutput context manager
1288
1289        Args:
1290            logger: The logger object to suppress
1291            log_levels: Levels of log handlers to disable.
1292        """
1293
1294        self._logger = logger
1295        self._log_levels = log_levels or [
1296            logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR,
1297            logging.CRITICAL
1298        ]
1299        if isinstance(self._log_levels, int):
1300            self._log_levels = [self._log_levels]
1301        self._handlers = copy.copy(self._logger.handlers)
1302
1303    def __enter__(self):
1304        for handler in self._handlers:
1305            if handler.level in self._log_levels:
1306                self._logger.removeHandler(handler)
1307        return self
1308
1309    def __exit__(self, *_):
1310        for handler in self._handlers:
1311            self._logger.addHandler(handler)
1312
1313
1314class BlockingTimer(object):
1315    """Context manager used to block until a specified amount of time has
1316     elapsed.
1317     """
1318    def __init__(self, secs):
1319        """Initializes a BlockingTimer
1320
1321        Args:
1322            secs: Number of seconds to wait before exiting
1323        """
1324        self._thread = threading.Timer(secs, lambda: None)
1325
1326    def __enter__(self):
1327        self._thread.start()
1328        return self
1329
1330    def __exit__(self, *_):
1331        self._thread.join()
1332
1333
1334def is_valid_ipv4_address(address):
1335    try:
1336        socket.inet_pton(socket.AF_INET, address)
1337    except AttributeError:  # no inet_pton here, sorry
1338        try:
1339            socket.inet_aton(address)
1340        except socket.error:
1341            return False
1342        return address.count('.') == 3
1343    except socket.error:  # not a valid address
1344        return False
1345
1346    return True
1347
1348
1349def is_valid_ipv6_address(address):
1350    if '%' in address:
1351        address = address.split('%')[0]
1352    try:
1353        socket.inet_pton(socket.AF_INET6, address)
1354    except socket.error:  # not a valid address
1355        return False
1356    return True
1357
1358
1359def merge_dicts(*dict_args):
1360    """ Merges args list of dictionaries into a single dictionary.
1361
1362    Args:
1363        dict_args: an args list of dictionaries to be merged. If multiple
1364            dictionaries share a key, the last in the list will appear in the
1365            final result.
1366    """
1367    result = {}
1368    for dictionary in dict_args:
1369        result.update(dictionary)
1370    return result
1371
1372
1373def ascii_string(uc_string):
1374    """Converts unicode string to ascii"""
1375    return str(uc_string).encode('ASCII')
1376
1377
1378def get_interface_ip_addresses(comm_channel, interface):
1379    """Gets all of the ip addresses, ipv4 and ipv6, associated with a
1380       particular interface name.
1381
1382    Args:
1383        comm_channel: How to send commands to a device.  Can be ssh, adb serial,
1384            etc.  Must have the run function implemented.
1385        interface: The interface name on the device, ie eth0
1386
1387    Returns:
1388        A list of dictionaries of the the various IP addresses:
1389            ipv4_private_local_addresses: Any 192.168, 172.16, or 10
1390                addresses
1391            ipv4_public_addresses: Any IPv4 public addresses
1392            ipv6_link_local_addresses: Any fe80:: addresses
1393            ipv6_private_local_addresses: Any fd00:: addresses
1394            ipv6_public_addresses: Any publicly routable addresses
1395    """
1396    ipv4_private_local_addresses = []
1397    ipv4_public_addresses = []
1398    ipv6_link_local_addresses = []
1399    ipv6_private_local_addresses = []
1400    ipv6_public_addresses = []
1401    all_interfaces_and_addresses = comm_channel.run(
1402        'ip -o addr | awk \'!/^[0-9]*: ?lo|link\/ether/ {gsub("/", " "); '
1403        'print $2" "$4}\'').stdout
1404    ifconfig_output = comm_channel.run('ifconfig %s' % interface).stdout
1405    for interface_line in all_interfaces_and_addresses.split('\n'):
1406        if interface != interface_line.split()[0]:
1407            continue
1408        on_device_ip = ipaddress.ip_address(interface_line.split()[1])
1409        if on_device_ip.version() == 4:
1410            if on_device_ip.is_private():
1411                ipv4_private_local_addresses.append(str(on_device_ip))
1412            elif on_device_ip.is_global():
1413                ipv4_public_addresses.append(str(on_device_ip))
1414        elif on_device_ip.version() == 6:
1415            if on_device_ip.is_link_local():
1416                ipv6_link_local_addresses.append(str(on_device_ip))
1417            elif on_device_ip.is_private():
1418                ipv6_private_local_addresses.append(str(on_device_ip))
1419            elif on_device_ip.is_global():
1420                ipv6_public_addresses.append(str(on_device_ip))
1421    return {
1422        'ipv4_private': ipv4_private_local_addresses,
1423        'ipv4_public': ipv4_public_addresses,
1424        'ipv6_link_local': ipv6_link_local_addresses,
1425        'ipv6_private_local': ipv6_private_local_addresses,
1426        'ipv6_public': ipv6_public_addresses
1427    }
1428
1429
1430def get_interface_based_on_ip(comm_channel, desired_ip_address):
1431    """Gets the interface for a particular IP
1432
1433    Args:
1434        comm_channel: How to send commands to a device.  Can be ssh, adb serial,
1435            etc.  Must have the run function implemented.
1436        desired_ip_address: The IP address that is being looked for on a device.
1437
1438    Returns:
1439        The name of the test interface.
1440    """
1441
1442    desired_ip_address = desired_ip_address.split('%', 1)[0]
1443    all_ips_and_interfaces = comm_channel.run(
1444        '(ip -o -4 addr show; ip -o -6 addr show) | '
1445        'awk \'{print $2" "$4}\'').stdout
1446    #ipv4_addresses = comm_channel.run(
1447    #    'ip -o -4 addr show| awk \'{print $2": "$4}\'').stdout
1448    #ipv6_addresses = comm_channel._ssh_session.run(
1449    #    'ip -o -6 addr show| awk \'{print $2": "$4}\'').stdout
1450    #if desired_ip_address in ipv4_addresses:
1451    #    ip_addresses_to_search = ipv4_addresses
1452    #elif desired_ip_address in ipv6_addresses:
1453    #    ip_addresses_to_search = ipv6_addresses
1454    for ip_address_and_interface in all_ips_and_interfaces.split('\n'):
1455        if desired_ip_address in ip_address_and_interface:
1456            return ip_address_and_interface.split()[1][:-1]
1457    return None
1458
1459
1460def renew_linux_ip_address(comm_channel, interface):
1461    comm_channel.run('sudo ifconfig %s down' % interface)
1462    comm_channel.run('sudo ifconfig %s up' % interface)
1463    comm_channel.run('sudo dhclient -r %s' % interface)
1464    comm_channel.run('sudo dhclient %s' % interface)
1465