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