Merge "[WifiTetheringTest] Increased the file download size" into oc-dev
diff --git a/acts/framework/acts/libs/ota/__init__.py b/acts/framework/acts/libs/ota/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts/framework/acts/libs/ota/__init__.py
diff --git a/acts/framework/acts/libs/ota/ota_runners/__init__.py b/acts/framework/acts/libs/ota/ota_runners/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts/framework/acts/libs/ota/ota_runners/__init__.py
diff --git a/acts/framework/acts/libs/ota/ota_runners/ota_runner.py b/acts/framework/acts/libs/ota/ota_runners/ota_runner.py
new file mode 100644
index 0000000..dd58943
--- /dev/null
+++ b/acts/framework/acts/libs/ota/ota_runners/ota_runner.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2017 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+import logging
+import time
+
+SL4A_SERVICE_SETUP_TIME = 5
+
+
+class OtaError(Exception):
+    """Raised when an error in the OTA Update process occurs."""
+
+
+class OtaRunner(object):
+    """The base class for all OTA Update Runners."""
+
+    def __init__(self, ota_tool, android_device):
+        self.ota_tool = ota_tool
+        self.android_device = android_device
+        self.serial = self.android_device.serial
+
+    def _update(self):
+        logging.info('Stopping services.')
+        self.android_device.stop_services()
+        logging.info('Beginning tool.')
+        self.ota_tool.update(self)
+        logging.info('Tool finished. Waiting for boot completion.')
+        self.android_device.wait_for_boot_completion()
+        logging.info('Boot completed. Rooting adb.')
+        self.android_device.root_adb()
+        logging.info('Root complete. Installing new SL4A.')
+        output = self.android_device.adb.install('-r %s' % self.get_sl4a_apk)
+        logging.info('SL4A install output: %s' % output)
+        time.sleep(SL4A_SERVICE_SETUP_TIME)
+        logging.info('Starting services.')
+        self.android_device.start_services()
+        logging.info('Services started. Running ota tool cleanup.')
+        self.ota_tool.cleanup(self)
+        logging.info('Cleanup complete.')
+
+    def can_update(self):
+        """Whether or not an update package is available for the device."""
+        return NotImplementedError()
+
+    def get_ota_package(self):
+        raise NotImplementedError()
+
+    def get_sl4a_apk(self):
+        raise NotImplementedError()
+
+
+class SingleUseOtaRunner(OtaRunner):
+    """A single use OtaRunner.
+
+    SingleUseOtaRunners can only be ran once. If a user attempts to run it more
+    than once, an error will be thrown. Users can avoid the error by checking
+    can_update() before calling update().
+    """
+
+    def __init__(self, ota_tool, android_device, ota_package, sl4a_apk):
+        super(SingleUseOtaRunner, self).__init__(ota_tool, android_device)
+        self._ota_package = ota_package
+        self._sl4a_apk = sl4a_apk
+        self._called = False
+
+    def can_update(self):
+        return not self._called
+
+    def update(self):
+        """Starts the update process."""
+        if not self.can_update():
+            raise OtaError('A SingleUseOtaTool instance cannot update a phone '
+                           'multiple times.')
+        self._called = True
+        self._update()
+
+    def get_ota_package(self):
+        return self._ota_package
+
+    def get_sl4a_apk(self):
+        return self._sl4a_apk
+
+
+class MultiUseOtaRunner(OtaRunner):
+    """A multiple use OtaRunner.
+
+    MultiUseOtaRunner can only be ran for as many times as there have been
+    packages provided to them. If a user attempts to run it more than the number
+    of provided packages, an error will be thrown. Users can avoid the error by
+    checking can_update() before calling update().
+    """
+
+    def __init__(self, ota_tool, android_device, ota_packages, sl4a_apks):
+        super(MultiUseOtaRunner, self).__init__(ota_tool, android_device)
+        self._ota_packages = ota_packages
+        self._sl4a_apks = sl4a_apks
+        self.current_update_number = 0
+
+    def can_update(self):
+        return not self.current_update_number == len(self._ota_packages)
+
+    def update(self):
+        """Starts the update process."""
+        if not self.can_update():
+            raise OtaError('This MultiUseOtaRunner has already updated all '
+                           'given packages onto the phone.')
+        self._update()
+        self.current_update_number += 1
+
+    def get_ota_package(self):
+        return self._ota_packages[self.current_update_number]
+
+    def get_sl4a_apk(self):
+        return self._sl4a_apks[self.current_update_number]
diff --git a/acts/framework/acts/libs/ota/ota_runners/ota_runner_factory.py b/acts/framework/acts/libs/ota/ota_runners/ota_runner_factory.py
new file mode 100644
index 0000000..fa6ab19
--- /dev/null
+++ b/acts/framework/acts/libs/ota/ota_runners/ota_runner_factory.py
@@ -0,0 +1,204 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2017 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+import logging
+
+from acts.config_parser import ActsConfigError
+from acts.libs.ota.ota_runners import ota_runner
+from acts.libs.ota.ota_tools import ota_tool_factory
+from acts.libs.ota.ota_tools import adb_sideload_ota_tool
+
+_bound_devices = {}
+
+DEFAULT_OTA_TOOL = adb_sideload_ota_tool.AdbSideloadOtaTool.__name__
+DEFAULT_OTA_COMMAND = 'adb'
+
+
+def create_all_from_configs(config, android_devices):
+    """Creates a new OtaTool for each given AndroidDevice.
+
+    After an OtaTool is assigned to a device, another OtaTool cannot be created
+    for that device. This will prevent OTA Update tests that accidentally flash
+    the same build onto a device more than once.
+
+    Args:
+        config: the ACTS config user_params.
+        android_devices: The devices to run an OTA Update on.
+
+    Returns:
+        A list of OtaRunners responsible for updating the given devices. The
+        indexes match the indexes of the corresponding AndroidDevice in
+        android_devices.
+    """
+    return [create_from_configs(config, ad) for ad in android_devices]
+
+
+def create_from_configs(config, android_device):
+    """Creates a new OtaTool for the given AndroidDevice.
+
+    After an OtaTool is assigned to a device, another OtaTool cannot be created
+    for that device. This will prevent OTA Update tests that accidentally flash
+    the same build onto a device more than once.
+
+    Args:
+        config: the ACTS config user_params.
+        android_device: The device to run the OTA Update on.
+
+    Returns:
+        An OtaRunner responsible for updating the given device.
+    """
+    # Default to adb sideload
+    try:
+        ota_tool_class_name = get_ota_value_from_config(
+            config, 'ota_tool', android_device)
+    except ActsConfigError:
+        ota_tool_class_name = DEFAULT_OTA_TOOL
+
+    if ota_tool_class_name not in config:
+        if ota_tool_class_name is not DEFAULT_OTA_TOOL:
+            raise ActsConfigError(
+                'If the ota_tool is overloaded, the path to the tool must be '
+                'added to the ACTS config file under {"OtaToolName": '
+                '"path/to/tool"} (in this case, {"%s": "path/to/tool"}.' %
+                ota_tool_class_name)
+        else:
+            command = DEFAULT_OTA_COMMAND
+    else:
+        command = config[ota_tool_class_name]
+        if type(command) is list:
+            # If file came as a list in the config.
+            if len(command) == 1:
+                command = command[0]
+            else:
+                raise ActsConfigError(
+                    'Config value for "%s" must be either a string or a list '
+                    'of exactly one element' % ota_tool_class_name)
+
+    ota_package = get_ota_value_from_config(config, 'ota_package',
+                                            android_device)
+    ota_sl4a = get_ota_value_from_config(config, 'ota_sl4a', android_device)
+    if type(ota_sl4a) != type(ota_package):
+        raise ActsConfigError(
+            'The ota_package and ota_sl4a must either both be strings, or '
+            'both be lists. Device with serial "%s" has mismatched types.' %
+            android_device.serial)
+    return create(ota_package, ota_sl4a, android_device, ota_tool_class_name,
+                  command)
+
+
+def create(ota_package,
+           ota_sl4a,
+           android_device,
+           ota_tool_class_name=DEFAULT_OTA_TOOL,
+           command=DEFAULT_OTA_COMMAND,
+           use_cached_runners=True):
+    """
+    Args:
+        ota_package: A string or list of strings corresponding to the
+            update.zip package location(s) for running an OTA update.
+        ota_sl4a: A string or list of strings corresponding to the
+            sl4a.apk package location(s) for running an OTA update.
+        ota_tool_class_name: The class name for the desired ota_tool
+        command: The command line tool name for the updater
+        android_device: The AndroidDevice to run the OTA Update on.
+        use_cached_runners: Whether or not to use runners cached by previous
+            create calls.
+
+    Returns:
+        An OtaRunner with the given properties from the arguments.
+    """
+    ota_tool = ota_tool_factory.create(ota_tool_class_name, command)
+    return create_from_package(ota_package, ota_sl4a, android_device, ota_tool,
+                               use_cached_runners)
+
+
+def create_from_package(ota_package,
+                        ota_sl4a,
+                        android_device,
+                        ota_tool,
+                        use_cached_runners=True):
+    """
+    Args:
+        ota_package: A string or list of strings corresponding to the
+            update.zip package location(s) for running an OTA update.
+        ota_sl4a: A string or list of strings corresponding to the
+            sl4a.apk package location(s) for running an OTA update.
+        ota_tool: The OtaTool to be paired with the returned OtaRunner
+        android_device: The AndroidDevice to run the OTA Update on.
+        use_cached_runners: Whether or not to use runners cached by previous
+            create calls.
+
+    Returns:
+        An OtaRunner with the given properties from the arguments.
+    """
+    if android_device in _bound_devices and use_cached_runners:
+        logging.warning('Android device %s has already been assigned an '
+                        'OtaRunner. Returning previously created runner.')
+        return _bound_devices[android_device]
+
+    if type(ota_package) != type(ota_sl4a):
+        raise TypeError(
+            'The ota_package and ota_sl4a must either both be strings, or '
+            'both be lists. Device with serial "%s" has requested mismatched '
+            'types.' % android_device.serial)
+
+    if type(ota_package) is str:
+        runner = ota_runner.SingleUseOtaRunner(ota_tool, android_device,
+                                               ota_package, ota_sl4a)
+    elif type(ota_package) is list:
+        runner = ota_runner.MultiUseOtaRunner(ota_tool, android_device,
+                                              ota_package, ota_sl4a)
+    else:
+        raise TypeError('The "ota_package" value in the acts config must be '
+                        'either a list or a string.')
+
+    _bound_devices[android_device] = runner
+    return runner
+
+
+def get_ota_value_from_config(config, key, android_device):
+    """Returns a key for the given AndroidDevice.
+
+    Args:
+        config: The ACTS config
+        key: The base key desired (ota_tool, ota_sl4a, or ota_package)
+        android_device: An AndroidDevice
+
+    Returns: The value at the specified key.
+    Throws: ActsConfigError if the value cannot be determined from the config.
+    """
+    suffix = ''
+    if 'ota_map' in config:
+        if android_device.serial in config['ota_map']:
+            suffix = '_%s' % config['ota_map'][android_device.serial]
+
+    ota_package_key = '%s%s' % (key, suffix)
+    if ota_package_key not in config:
+        if suffix is not '':
+            raise ActsConfigError(
+                'Asked for an OTA Update without specifying a required value. '
+                '"ota_map" has entry {"%s": "%s"}, but there is no '
+                'corresponding entry {"%s":"/path/to/file"} found within the '
+                'ACTS config.' % (android_device.serial, suffix[1:],
+                                  ota_package_key))
+        else:
+            raise ActsConfigError(
+                'Asked for an OTA Update without specifying a required value. '
+                '"ota_map" does not exist or have a key for serial "%s", and '
+                'the default value entry "%s" cannot be found within the ACTS '
+                'config.' % (android_device.serial, ota_package_key))
+
+    return config[ota_package_key]
diff --git a/acts/framework/acts/libs/ota/ota_tools/__init__.py b/acts/framework/acts/libs/ota/ota_tools/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts/framework/acts/libs/ota/ota_tools/__init__.py
diff --git a/acts/framework/acts/libs/ota/ota_tools/adb_sideload_ota_tool.py b/acts/framework/acts/libs/ota/ota_tools/adb_sideload_ota_tool.py
new file mode 100644
index 0000000..f94a762
--- /dev/null
+++ b/acts/framework/acts/libs/ota/ota_tools/adb_sideload_ota_tool.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2017 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+import logging
+
+from acts.libs.ota.ota_tools.ota_tool import OtaTool
+
+# OTA Packages can be upwards of 1 GB. This may take some time to transfer over
+# USB 2.0.
+PUSH_TIMEOUT = 10 * 60
+
+
+class AdbSideloadOtaTool(OtaTool):
+    """Updates an AndroidDevice using adb sideload."""
+
+    def __init__(self, ignored_command):
+        # "command" is ignored. The ACTS adb version is used to prevent
+        # differing adb versions from constantly killing adbd.
+        super(AdbSideloadOtaTool, self).__init__(ignored_command)
+
+    def update(self, ota_runner):
+        logging.info('Rooting adb')
+        ota_runner.android_device.root_adb()
+        logging.info('Rebooting to sideload')
+        ota_runner.android_device.adb.reboot('sideload')
+        ota_runner.android_device.adb.wait_for_sideload()
+        logging.info('Sideloading ota package')
+        package_path = ota_runner.get_ota_package()
+        logging.info('Running adb sideload with package "%s"' % package_path)
+        sideload_result = ota_runner.android_device.adb.sideload(
+            package_path, timeout=PUSH_TIMEOUT)
+        logging.info('Sideload output: %s' % sideload_result)
+        logging.info('Sideload complete. Waiting for device to come back up.')
+        ota_runner.android_device.adb.wait_for_recovery()
+        ota_runner.android_device.adb.reboot()
+        logging.info('Device is up. Update complete.')
diff --git a/acts/framework/acts/libs/ota/ota_tools/ota_tool.py b/acts/framework/acts/libs/ota/ota_tools/ota_tool.py
new file mode 100644
index 0000000..e51fe6b
--- /dev/null
+++ b/acts/framework/acts/libs/ota/ota_tools/ota_tool.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2017 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+
+class OtaTool(object):
+    """A Wrapper for an OTA Update command or tool.
+
+    Each OtaTool acts as a facade to the underlying command or tool used to
+    update the device.
+    """
+
+    def __init__(self, command):
+        """Creates an OTA Update tool with the given properties.
+
+        Args:
+            command: A string that is used as the command line tool
+        """
+        self.command = command
+
+    def update(self, ota_runner):
+        """Begins the OTA Update. Returns after the update has installed.
+
+        Args:
+            ota_runner: The OTA Runner that handles the device information.
+        """
+        raise NotImplementedError()
+
+    def cleanup(self, ota_runner):
+        """A cleanup method for the OTA Tool to run after the update completes.
+
+        Args:
+            ota_runner: The OTA Runner that handles the device information.
+        """
+        pass
diff --git a/acts/framework/acts/libs/ota/ota_tools/ota_tool_factory.py b/acts/framework/acts/libs/ota/ota_tools/ota_tool_factory.py
new file mode 100644
index 0000000..ac81646
--- /dev/null
+++ b/acts/framework/acts/libs/ota/ota_tools/ota_tool_factory.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2017 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+from acts.libs.ota.ota_tools.adb_sideload_ota_tool import AdbSideloadOtaTool
+from acts.libs.ota.ota_tools.update_device_ota_tool import UpdateDeviceOtaTool
+
+_CONSTRUCTORS = {
+    AdbSideloadOtaTool.__name__: lambda command: AdbSideloadOtaTool(command),
+    UpdateDeviceOtaTool.__name__: lambda command: UpdateDeviceOtaTool(command),
+}
+_constructed_tools = {}
+
+
+def create(ota_tool_class, command):
+    """Returns an OtaTool with the given class name.
+
+    If the tool has already been created, the existing instance will be
+    returned.
+
+    Args:
+        ota_tool_class: the class/type of the tool you wish to use.
+        command: the command line tool being used.
+
+    Returns:
+        An OtaTool.
+    """
+    if ota_tool_class in _constructed_tools:
+        return _constructed_tools[ota_tool_class]
+
+    if ota_tool_class not in _CONSTRUCTORS:
+        raise KeyError('Given Ota Tool class name does not match a known '
+                       'name. Found "%s". Expected any of %s. If this tool '
+                       'does exist, add it to the _CONSTRUCTORS dict in this '
+                       'module.' % (ota_tool_class, _CONSTRUCTORS.keys()))
+
+    new_update_tool = _CONSTRUCTORS[ota_tool_class](command)
+    _constructed_tools[ota_tool_class] = new_update_tool
+
+    return new_update_tool
diff --git a/acts/framework/acts/libs/ota/ota_tools/update_device_ota_tool.py b/acts/framework/acts/libs/ota/ota_tools/update_device_ota_tool.py
new file mode 100644
index 0000000..0ab9091
--- /dev/null
+++ b/acts/framework/acts/libs/ota/ota_tools/update_device_ota_tool.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2017 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+import logging
+import os
+import shutil
+import tempfile
+
+from acts.libs.ota.ota_tools import ota_tool
+from acts.libs.proc import job
+from acts import utils
+
+# OTA Packages can be upwards of 1 GB. This may take some time to transfer over
+# USB 2.0. A/B devices must also complete the update in the background.
+UPDATE_TIMEOUT = 20 * 60
+UPDATE_LOCATION = '/data/ota_package/update.zip'
+
+
+class UpdateDeviceOtaTool(ota_tool.OtaTool):
+    """Runs an OTA Update with system/update_engine/scripts/update_device.py."""
+
+    def __init__(self, command):
+        super(UpdateDeviceOtaTool, self).__init__(command)
+
+        self.unzip_path = tempfile.mkdtemp()
+        utils.unzip_maintain_permissions(self.command, self.unzip_path)
+
+        self.command = os.path.join(self.unzip_path, 'update_device.py')
+
+    def update(self, ota_runner):
+        logging.info('Forcing adb to be in root mode.')
+        ota_runner.android_device.root_adb()
+        update_command = '%s -s %s %s' % (self.command, ota_runner.serial,
+                                          ota_runner.get_ota_package())
+        logging.info('Running %s' % update_command)
+        result = job.run(update_command, timeout=UPDATE_TIMEOUT)
+        logging.info('Output: %s' % result.stdout)
+
+        logging.info('Rebooting device for update to go live.')
+        ota_runner.android_device.adb.reboot()
+        logging.info('Reboot sent.')
+
+    def __del__(self):
+        """Delete the unzipped update_device folder before ACTS exits."""
+        shutil.rmtree(self.unzip_path)
diff --git a/acts/framework/acts/libs/ota/ota_updater.py b/acts/framework/acts/libs/ota/ota_updater.py
new file mode 100644
index 0000000..ed300aa
--- /dev/null
+++ b/acts/framework/acts/libs/ota/ota_updater.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2017 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+from acts.libs.ota.ota_runners import ota_runner_factory
+
+# Maps AndroidDevices to OtaRunners
+ota_runners = {}
+
+
+def initialize(user_params, android_devices):
+    """Initialize OtaRunners for each device.
+
+    Args:
+        user_params: The user_params from the ACTS config.
+        android_devices: The android_devices in the test.
+    """
+    for ad in android_devices:
+        ota_runners[ad] = ota_runner_factory.create_from_configs(
+            user_params, ad)
+
+
+def _check_initialization(android_device):
+    """Check if a given device was initialized."""
+    if android_device not in ota_runners:
+        raise KeyError('Android Device with serial "%s" has not been '
+                       'initialized for OTA Updates. Did you forget to call'
+                       'ota_updater.initialize()?' % android_device.serial)
+
+
+def update(android_device, ignore_update_errors=False):
+    """Update a given AndroidDevice.
+
+    Args:
+        android_device: The device to update
+        ignore_update_errors: Whether or not to ignore update errors such as
+           no more updates available for a given device. Default is false.
+    Throws:
+        OtaError if ignore_update_errors is false and the OtaRunner has run out
+        of packages to update the phone with.
+    """
+    _check_initialization(android_device)
+    try:
+        ota_runners[android_device].update()
+    except:
+        if ignore_update_errors:
+            return
+        raise
+
+
+def can_update(android_device):
+    """Whether or not a device can be updated."""
+    _check_initialization(android_device)
+    return ota_runners[android_device].can_update()
diff --git a/acts/framework/acts/test_utils/tel/tel_test_utils.py b/acts/framework/acts/test_utils/tel/tel_test_utils.py
index 5732351..c778466 100644
--- a/acts/framework/acts/test_utils/tel/tel_test_utils.py
+++ b/acts/framework/acts/test_utils/tel/tel_test_utils.py
@@ -4575,7 +4575,10 @@
 
     """
     ad.log.debug("Ensuring no tcpdump is running in background")
-    ad.adb.shell("killall -9 tcpdump")
+    try:
+        ad.adb.shell("killall -9 tcpdump")
+    except AdbError:
+        self.log.warn("Killing existing tcpdump processes failed")
     begin_time = epoch_to_log_line_timestamp(get_current_epoch_time())
     begin_time = normalize_log_line_timestamp(begin_time)
     file_name = "/sdcard/tcpdump{}{}{}.pcap".format(ad.serial, test_name,
diff --git a/acts/framework/acts/utils.py b/acts/framework/acts/utils.py
index c5d401b..5faabec 100755
--- a/acts/framework/acts/utils.py
+++ b/acts/framework/acts/utils.py
@@ -28,6 +28,7 @@
 import subprocess
 import time
 import traceback
+import zipfile
 
 # File name length is limited to 255 chars on some OS, so we need to make sure
 # the file names we output fits within the limit.
@@ -807,3 +808,28 @@
         return False
     finally:
         ad.adb.shell("rm /data/ping.txt", timeout=10, ignore_status=True)
+
+
+def unzip_maintain_permissions(zip_path, extract_location):
+    """Unzip a .zip file while maintaining permissions.
+
+    Args:
+        zip_path: The path to the zipped file.
+        extract_location: the directory to extract to.
+    """
+    with zipfile.ZipFile(zip_path, 'r') as zip_file:
+        for info in zip_file.infolist():
+            _extract_file(zip_file, info, extract_location)
+
+
+def _extract_file(zip_file, zip_info, extract_location):
+    """Extracts a single entry from a ZipFile while maintaining permissions.
+
+    Args:
+        zip_file: A zipfile.ZipFile.
+        zip_info: A ZipInfo object from zip_file.
+        extract_location: The directory to extract to.
+    """
+    out_path = zip_file.extract(zip_info.filename, path=extract_location)
+    perm = zip_info.external_attr >> 16
+    os.chmod(out_path, perm)
diff --git a/acts/tests/google/wifi/WifiEnterpriseTest.py b/acts/tests/google/wifi/WifiEnterpriseTest.py
index b1a5391..785d91f 100755
--- a/acts/tests/google/wifi/WifiEnterpriseTest.py
+++ b/acts/tests/google/wifi/WifiEnterpriseTest.py
@@ -22,6 +22,8 @@
 from acts import base_test
 from acts import signals
 from acts.test_decorators import test_tracker_info
+from acts.test_utils.tel.tel_test_utils import start_adb_tcpdump
+from acts.test_utils.tel.tel_test_utils import stop_adb_tcpdump
 from acts.test_utils.wifi import wifi_test_utils as wutils
 
 WifiEnums = wutils.WifiEnums
@@ -127,6 +129,8 @@
         del self.config_passpoint_ttls[WifiEnums.SSID_KEY]
         # Set screen lock password so ConfigStore is unlocked.
         self.dut.droid.setDevicePassword(self.device_password)
+        self.tcpdump_pid = None
+        self.tcpdump_file = None
 
     def teardown_class(self):
         wutils.reset_wifi(self.dut)
@@ -139,8 +143,16 @@
         self.dut.droid.wakeUpNow()
         wutils.reset_wifi(self.dut)
         self.dut.ed.clear_all_events()
+        (self.tcpdump_pid, self.tcpdump_file) = start_adb_tcpdump(
+            self.dut, self.test_name, mask='all')
 
     def teardown_test(self):
+        if self.tcpdump_pid:
+            stop_adb_tcpdump(self.dut,
+                             self.tcpdump_pid,
+                             self.tcpdump_file,
+                             pull_tcpdump=True)
+            self.tcpdump_pid = None
         self.dut.droid.wakeLockRelease()
         self.dut.droid.goToSleepNow()
         self.dut.droid.wifiStopTrackingStateChange()
diff --git a/acts/tests/sample/OtaSampleTest.py b/acts/tests/sample/OtaSampleTest.py
new file mode 100644
index 0000000..aeb735e
--- /dev/null
+++ b/acts/tests/sample/OtaSampleTest.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2017 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+from acts import base_test
+from acts.libs.ota import ota_updater
+
+
+class OtaSampleTest(base_test.BaseTestClass):
+    """Demonstrates an example OTA Update test."""
+
+    def setup_class(self):
+        ota_updater.initialize(self.user_params, self.android_devices)
+        self.dut = self.android_devices[0]
+
+    def test_my_test(self):
+        self.pre_ota()
+        ota_updater.update(self.dut)
+        self.post_ota()
+
+    def pre_ota(self):
+        pass
+
+    def post_ota(self):
+        pass