Snap for 4404534 from 0de194cfeebe455954ccfbc2a5b51d978b7778a7 to oc-mr1-release

Change-Id: Icffda05a0ed9e6a85bb160936b1642f8d5661c5f
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 5c398f8..e13b964 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
 
 from acts.controllers import adb
 
@@ -846,3 +847,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/bt/ota/BtOtaTest.py b/acts/tests/google/bt/ota/BtOtaTest.py
new file mode 100644
index 0000000..91e51bb
--- /dev/null
+++ b/acts/tests/google/bt/ota/BtOtaTest.py
@@ -0,0 +1,137 @@
+# 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.
+"""
+Test script for Bluetooth OTA testing.
+"""
+
+from acts.libs.ota import ota_updater
+from acts.test_decorators import test_tracker_info
+from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
+from acts.test_utils.bt.bt_test_utils import pair_pri_to_sec
+from acts import signals
+
+
+class BtOtaTest(BluetoothBaseTest):
+    def setup_class(self):
+        super(BtOtaTest, self).setup_class()
+        ota_updater.initialize(self.user_params, self.android_devices)
+        self.dut = self.android_devices[0]
+        self.pre_ota_name = self.dut.droid.bluetoothGetLocalName()
+        self.pre_ota_address = self.dut.droid.bluetoothGetLocalAddress()
+        self.sec_address = self.android_devices[
+            1].droid.bluetoothGetLocalAddress()
+
+        # Pairing devices
+        if not pair_pri_to_sec(self.dut, self.android_devices[1]):
+            raise signals.TestSkipClass(
+                "Failed to bond devices prior to update")
+
+        #Run OTA below, if ota fails then abort all tests
+        try:
+            ota_updater.update(self.dut)
+        except Exception as err:
+            raise signals.TestSkipClass(
+                "Failed up apply OTA update. Aborting tests")
+
+    @BluetoothBaseTest.bt_test_wrap
+    @test_tracker_info(uuid='57545ef0-2c2e-463c-9dbf-28da73cc76df')
+    def test_device_name_persists(self):
+        """Test device name persists after OTA update
+
+        Test device name persists after OTA update
+
+        Steps:
+        1. Verify pre OTA device name matches post OTA device name
+
+        Expected Result:
+        Bluetooth Device name persists
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: OTA
+        Priority: 2
+        """
+        return self.pre_ota_name == self.dut.droid.bluetoothGetLocalName()
+
+    @BluetoothBaseTest.bt_test_wrap
+    @test_tracker_info(uuid='1fd5e1a5-d930-499c-aebc-c1872ab49568')
+    def test_device_address_persists(self):
+        """Test device address persists after OTA update
+
+        Test device address persists after OTA update
+
+        Steps:
+        1. Verify pre OTA device address matches post OTA device address
+
+        Expected Result:
+        Bluetooth Device address persists
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: OTA
+        Priority: 2
+        """
+        return self.pre_ota_address == self.dut.droid.bluetoothGetLocalAddress(
+        )
+
+    @BluetoothBaseTest.bt_test_wrap
+    @test_tracker_info(uuid='2e6704e6-3df0-43fb-8425-41ff841d7473')
+    def test_bluetooth_state_persists(self):
+        """Test device Bluetooth state persists after OTA update
+
+        Test device Bluetooth state persists after OTA update
+
+        Steps:
+        1. Verify post OTA Bluetooth state is on
+
+        Expected Result:
+        Bluetooth Device Bluetooth state is on
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: OTA
+        Priority: 2
+        """
+        return self.dut.droid.bluetoothCheckState()
+
+    @BluetoothBaseTest.bt_test_wrap
+    @test_tracker_info(uuid='eb1c0a22-4b4e-4984-af17-ace3bcd203de')
+    def test_bonded_devices_persist(self):
+        """Test device bonded devices persists after OTA update
+
+        Test device address persists after OTA update
+
+        Steps:
+        1. Verify pre OTA device bonded devices matches post OTA device
+        bonded devices
+
+        Expected Result:
+        Bluetooth Device bonded devices persists
+
+        Returns:
+          Pass if True
+          Fail if False
+
+        TAGS: OTA
+        Priority: 1
+        """
+        bonded_devices = self.dut.droid.bluetoothGetBondedDevices()
+        for b in bonded_devices:
+            if b['address'] == self.sec_address:
+                return True
+        return False
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/google/wifi/WifiTetheringTest.py b/acts/tests/google/wifi/WifiTetheringTest.py
index 57a8e66..ef79f3d 100644
--- a/acts/tests/google/wifi/WifiTetheringTest.py
+++ b/acts/tests/google/wifi/WifiTetheringTest.py
@@ -44,7 +44,7 @@
 
         self.convert_byte_to_mb = 1024.0 * 1024.0
         self.new_ssid = "wifi_tethering_test2"
-        self.data_usage_error = 0.3
+        self.data_usage_error = 1
 
         self.hotspot_device = self.android_devices[0]
         self.tethered_devices = self.android_devices[1:]
@@ -89,6 +89,8 @@
     def on_fail(self, test_name, begin_time):
         """ Collect bug report on failure """
         self.hotspot_device.take_bug_report(test_name, begin_time)
+        for ad in self.tethered_devices:
+            ad.take_bug_report(test_name, begin_time)
 
     """ Helper functions """
 
@@ -143,6 +145,7 @@
         """
         default_route_substr = "::/0 -> "
         link_properties = dut.droid.connectivityGetActiveLinkProperties()
+        self.log.info("LINK PROPERTIES:\n%s\n" % link_properties)
         return link_properties and default_route_substr in link_properties
 
     def _verify_ipv6_tethering(self, dut):
@@ -182,6 +185,8 @@
         for _ in range(50):
             dut_id = random.randint(0, len(self.tethered_devices)-1)
             dut = self.tethered_devices[dut_id]
+            # wait for 1 sec between connect & disconnect stress test
+            time.sleep(1)
             if device_connected[dut_id]:
                 wutils.wifi_forget_network(dut, self.network["SSID"])
             else:
@@ -271,12 +276,12 @@
 
         Steps:
             1. Start wifi tethering on hotspot device
-            2. Verify IPv6 address on hotspot device
+            2. Verify IPv6 address on hotspot device (VZW & TMO only)
             3. Connect tethered device to hotspot device
-            4. Verify IPv6 address on the client's link properties
-            5. Verify ping on client using ping6 which should pass
+            4. Verify IPv6 address on the client's link properties (VZW only)
+            5. Verify ping on client using ping6 which should pass (VZW only)
             6. Disable mobile data on provider and verify that link properties
-               does not have IPv6 address and default route
+               does not have IPv6 address and default route (VZW only)
         """
         # Start wifi tethering on the hotspot device
         wutils.toggle_wifi_off_and_on(self.hotspot_device)
@@ -320,14 +325,15 @@
 
         result = self._find_ipv6_default_route(self.tethered_devices[0])
         self.hotspot_device.droid.telephonyToggleDataConnection(True)
-        if not result:
+        if result:
             asserts.fail("Found IPv6 default route in link properties:Data off")
+        self.log.info("Did not find IPv6 address in link properties")
 
         # Disable wifi tethering
         wutils.stop_wifi_tethering(self.hotspot_device)
 
     @test_tracker_info(uuid="110b61d1-8af2-4589-8413-11beac7a3025")
-    def test_wifi_tethering_2ghz_traffic_between_2tethered_devices(self):
+    def wifi_tethering_2ghz_traffic_between_2tethered_devices(self):
         """ Steps:
 
             1. Start wifi hotspot with 2G band
@@ -341,7 +347,7 @@
         wutils.stop_wifi_tethering(self.hotspot_device)
 
     @test_tracker_info(uuid="953f6e2e-27bd-4b73-85a6-d2eaa4e755d5")
-    def test_wifi_tethering_5ghz_traffic_between_2tethered_devices(self):
+    def wifi_tethering_5ghz_traffic_between_2tethered_devices(self):
         """ Steps:
 
             1. Start wifi hotspot with 5ghz band
@@ -435,7 +441,8 @@
         end_time = int(time.time() * 1000)
         bytes_before_download = dut.droid.connectivityGetRxBytesForDevice(
             subscriber_id, 0, end_time)
-        self.log.info("Bytes before download %s" % bytes_before_download)
+        self.log.info("Data usage before download: %s MB" %
+                      (bytes_before_download/self.convert_byte_to_mb))
 
         # download file
         self.log.info("Download file of size %sMB" % self.file_size)
@@ -446,7 +453,8 @@
         end_time = int(time.time() * 1000)
         bytes_after_download = dut.droid.connectivityGetRxBytesForDevice(
             subscriber_id, 0, end_time)
-        self.log.info("Bytes after download %s" % bytes_after_download)
+        self.log.info("Data usage after download: %s MB" %
+                      (bytes_after_download/self.convert_byte_to_mb))
 
         bytes_diff = bytes_after_download - bytes_before_download
         wutils.stop_wifi_tethering(self.hotspot_device)
@@ -461,9 +469,9 @@
     def test_wifi_tethering_data_usage_limit(self):
         """ Steps:
 
-            1. Set the data usage limit to current data usage + 2MB
+            1. Set the data usage limit to current data usage + 10MB
             2. Start wifi tethering and connect a dut to the SSID
-            3. Download 5MB data on tethered device
+            3. Download 20MB data on tethered device
                a. file download should stop
                b. tethered device will lose internet connectivity
                c. data usage limit reached message should be displayed
@@ -472,7 +480,7 @@
         """
         wutils.toggle_wifi_off_and_on(self.hotspot_device)
         dut = self.hotspot_device
-        data_usage_2mb = 2 * self.convert_byte_to_mb
+        data_usage_inc = 10 * self.convert_byte_to_mb
         subscriber_id = dut.droid.telephonyGetSubscriberId()
 
         self._start_wifi_tethering()
@@ -483,11 +491,11 @@
         old_data_usage = dut.droid.connectivityQuerySummaryForDevice(
             subscriber_id, 0, end_time)
 
-        # set data usage limit to current usage limit + 2MB
+        # set data usage limit to current usage limit + 10MB
         dut.droid.connectivitySetDataUsageLimit(
-            subscriber_id, str(int(old_data_usage + data_usage_2mb)))
+            subscriber_id, str(int(old_data_usage + data_usage_inc)))
 
-        # download file - size 5MB
+        # download file - size 20MB
         http_file_download_by_chrome(self.tethered_devices[0],
                                      self.download_file,
                                      timeout=120)
@@ -503,8 +511,10 @@
         dut.droid.connectivityFactoryResetNetworkPolicies(subscriber_id)
         wutils.stop_wifi_tethering(self.hotspot_device)
 
-        old_data_usage = (old_data_usage+data_usage_2mb)/self.convert_byte_to_mb
+        old_data_usage = (old_data_usage+data_usage_inc)/self.convert_byte_to_mb
         new_data_usage = new_data_usage/self.convert_byte_to_mb
+        self.log.info("Expected data usage: %s MB" % old_data_usage)
+        self.log.info("Actual data usage: %s MB" % new_data_usage)
 
         return (new_data_usage-old_data_usage) < self.data_usage_error
 
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